Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rjdellecese/confect/llms.txt

Use this file to discover all available pages before exploring further.

Functions are the core building blocks of your Confect backend. Confect supports three types of functions: queries for reading data, mutations for writing data, and actions for external integrations.

Function Types

Queries

Read data from the database with real-time updates

Mutations

Write data to the database transactionally

Actions

Perform external operations and side effects

Creating Functions

Functions in Confect follow a two-phase pattern: specification and implementation.

Function Specification

First, define your function’s interface using FunctionSpec:
confect/api.ts
import { FunctionSpec } from "@confect/core";
import { Schema } from "effect";

export const getUserSpec = FunctionSpec.query({
  args: Schema.Struct({
    userId: Schema.String,
  }),
  returns: Schema.Struct({
    id: Schema.String,
    name: Schema.String,
    email: Schema.String,
  }),
});

Function Implementation

Then, implement the function logic using FunctionImpl:
confect/users.ts
import { FunctionImpl } from "@confect/server";
import { Effect } from "effect";
import { api } from "./api";

export const getUser = FunctionImpl.make(
  api,
  "users",
  "get",
  ({ userId }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      
      const user = yield* db
        .table("users")
        .get(userId);
      
      if (!user) {
        return yield* Effect.fail(new Error("User not found"));
      }
      
      return {
        id: user._id,
        name: user.name,
        email: user.email,
      };
    }).pipe(Effect.orDie)
);

Query Functions

Queries are read-only functions that can access the database and run in real-time.

Available Services

DatabaseReader

Read data from any table

Auth

Access user authentication info

StorageReader

Get URLs for stored files

QueryRunner

Run other queries

Query Example

import { DatabaseReader, Auth } from "@confect/server";
import { Effect } from "effect";

const listUserPosts = FunctionImpl.make(
  api,
  "posts",
  "listByUser",
  ({ limit = 10 }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      const auth = yield* Auth;
      
      const identity = yield* auth.getUserIdentity;
      
      const posts = yield* db
        .table("posts")
        .index("by_author", (q) => q.eq("authorId", identity.subject))
        .take(limit)
        .collect();
      
      return posts;
    }).pipe(Effect.orDie)
);
Queries are cached and automatically rerun when data changes, providing real-time updates to clients.

Mutation Functions

Mutations are transactional functions that can read and write to the database.

Available Services

DatabaseReader

Read data from any table

DatabaseWriter

Write data to any table

Auth

Access user authentication

Scheduler

Schedule future functions

StorageReader

Read file storage

StorageWriter

Write to file storage

QueryRunner

Run queries

MutationRunner

Run other mutations

Mutation Example

import { DatabaseWriter, Auth, Scheduler } from "@confect/server";
import { Effect, Duration } from "effect";

const createPost = FunctionImpl.make(
  api,
  "posts",
  "create",
  ({ title, content }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseWriter<typeof databaseSchema>();
      const auth = yield* Auth;
      const scheduler = yield* Scheduler;
      
      const identity = yield* auth.getUserIdentity;
      
      const postId = yield* db.table("posts").insert({
        title,
        content,
        authorId: identity.subject,
        createdAt: Date.now(),
        published: false,
      });
      
      // Schedule publish in 1 hour
      yield* scheduler.runAfter(
        Duration.hours(1),
        refs.internal.posts.publish,
        { postId }
      );
      
      return { id: postId };
    }).pipe(Effect.orDie)
);
Mutations are transactional - if any part fails, the entire mutation is rolled back.

Action Functions

Actions can perform external operations like API calls, send emails, or access Node.js APIs.

Convex Actions

Standard actions run in the Convex runtime:
import { ActionRunner, Auth } from "@confect/server";
import { Effect } from "effect";

const sendEmail = FunctionImpl.make(
  api,
  "notifications",
  "sendEmail",
  ({ to, subject, body }) =>
    Effect.gen(function* () {
      const auth = yield* Auth;
      const runMutation = yield* MutationRunner;
      
      const identity = yield* auth.getUserIdentity;
      
      // Call external email service
      const response = yield* Effect.tryPromise(() =>
        fetch("https://api.emailservice.com/send", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ to, subject, body }),
        })
      );
      
      // Log the email
      yield* runMutation(
        refs.internal.emailLog.create,
        { to, subject, sentAt: Date.now() }
      );
      
      return { success: true };
    }).pipe(Effect.orDie)
);

Node Actions

Node actions have access to the full Node.js runtime:
import { FunctionSpec } from "@confect/core";
import { Schema } from "effect";

export const processFile = FunctionSpec.nodeAction({
  args: Schema.Struct({
    fileId: Schema.String,
  }),
  returns: Schema.Struct({
    processed: Schema.Boolean,
  }),
});
import { FileSystem } from "@effect/platform-node";
import { Effect } from "effect";

const processFileImpl = FunctionImpl.make(
  api,
  "files",
  "process",
  ({ fileId }) =>
    Effect.gen(function* () {
      const fs = yield* FileSystem.FileSystem;
      const storage = yield* StorageActionWriter;
      
      // Download file from storage
      const blob = yield* storage.get(fileId);
      
      // Process with Node.js APIs
      const buffer = Buffer.from(await blob.arrayBuffer());
      // ... process buffer ...
      
      return { processed: true };
    }).pipe(Effect.orDie)
);
Node actions are the only function type that can access the Node.js runtime, including the filesystem, crypto, and other Node-specific APIs.

Function Visibility

Functions can be public or internal:

Public Functions

const publicSpec = FunctionSpec.query({
  args: Schema.Struct({ id: Schema.String }),
  returns: Schema.Struct({ data: Schema.String }),
  // Public by default
});
Public functions can be called from client applications.

Internal Functions

const internalSpec = FunctionSpec.internalQuery({
  args: Schema.Struct({ id: Schema.String }),
  returns: Schema.Struct({ data: Schema.String }),
});
Internal functions can only be called from other backend functions.

Error Handling

Use Effect’s error handling for robust function implementations:
import { Schema } from "effect";

class UserNotFoundError extends Schema.TaggedError<UserNotFoundError>()()
  "UserNotFoundError",
  { userId: Schema.String }
) {
  override get message() {
    return `User ${this.userId} not found`;
  }
}

const getUser = FunctionImpl.make(
  api,
  "users",
  "get",
  ({ userId }) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      
      const user = yield* db
        .table("users")
        .get(userId)
        .pipe(
          Effect.mapError(() => new UserNotFoundError({ userId }))
        );
      
      return user;
    }).pipe(
      Effect.catchTag("UserNotFoundError", (error) =>
        Effect.fail(new Error(error.message))
      )
    )
);

Composing Functions

Use function runners to compose functions:
const getUserWithPosts = FunctionImpl.make(
  api,
  "users",
  "getWithPosts",
  ({ userId }) =>
    Effect.gen(function* () {
      const runQuery = yield* QueryRunner;
      
      const user = yield* runQuery(
        refs.internal.users.get,
        { userId }
      );
      
      const posts = yield* runQuery(
        refs.internal.posts.listByUser,
        { userId }
      );
      
      return { user, posts };
    }).pipe(Effect.orDie)
);

Best Practices

1

Keep Functions Small

Each function should do one thing well. Compose multiple functions for complex operations.
2

Use Type-Safe Schemas

Define comprehensive schemas for arguments and returns to catch errors at compile time.
3

Handle Errors Explicitly

Use Effect’s error handling to make error cases visible in types.
4

Leverage Services

Use Confect services instead of raw context for better testability.

Next Steps

Database

Learn about database operations

Node Actions

Explore Node.js runtime actions

Scheduling

Schedule functions to run later

Authentication

Add user authentication

Build docs developers (and LLMs) love