Skip to main content
Confect functions are defined as a spec and impl pair. The spec declares each function’s name, arguments, and return type. The impl provides the handler logic.

Function types

ConstructorDescription
FunctionSpec.publicQueryPublic query function
FunctionSpec.publicMutationPublic mutation function
FunctionSpec.publicActionPublic action function
FunctionSpec.internalQueryInternal query function
FunctionSpec.internalMutationInternal mutation function
FunctionSpec.internalActionInternal action function
For Node.js actions, see Node Actions. To integrate plain Convex functions (for use with Convex components or other libraries), see Plain Convex Functions.

Defining a spec

Each function spec defines the function’s name, arguments schema, and returns schema. The args, returns, and (optional) error schemas are passed as () => Schema callbacks, evaluated lazily the first time the function is invoked. Function specs are added to a GroupSpec and default-exported from a *.spec.ts file. See The Spec/Impl Model for a full walkthrough.
confect/notes.spec.ts
import { GroupSpec, FunctionSpec } from "@confect/core";
import * as Schema from "effect/Schema";

import notes from "./_generated/tables/notes";

const list = FunctionSpec.publicQuery({
  name: "list",
  args: () => Schema.Struct({}),
  returns: () => Schema.Array(notes.Doc),
});

export default GroupSpec.make().addFunction(list);
Run confect codegen after adding or changing specs.

Typed errors

A spec can also declare an optional error schema. When it does, the corresponding handler’s Effect error channel is typed as that schema, and the failure is decoded for callers at every call site (React hooks, JS clients, and tests). See Error Handling for the full walkthrough.
import { FunctionSpec } from "@confect/core";
import * as Schema from "effect/Schema";

import { Id } from "./_generated/id";
import notes from "./_generated/tables/notes";

class NoteNotFound extends Schema.TaggedError<NoteNotFound>()("NoteNotFound", {
  noteId: Id("notes"),
}) {}

FunctionSpec.publicQuery({
  name: "getOrFail",
  args: () => Schema.Struct({ noteId: Id("notes") }),
  returns: () => notes.Doc,
  error: () => NoteNotFound,
});

Implementing functions

Each function impl contains a handler that implements the function’s logic. Function impls are composed into group impls using Effect layers. The handler receives the decoded arguments as its first parameter and returns an Effect. Use the generated services (like DatabaseReader, DatabaseWriter, Auth, etc.) inside your handler to interact with Convex.
confect/notes.impl.ts
import { FunctionImpl, GroupImpl } from "@confect/server";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";

import databaseSchema from "./_generated/schema";
import { DatabaseReader } from "./_generated/services";
import notes from "./notes.spec";

const list = FunctionImpl.make(databaseSchema, notes, "list", () =>
  Effect.gen(function* () {
    const reader = yield* DatabaseReader;

    return yield* reader
      .table("notes")
      .index("by_creation_time", "desc")
      .collect();
  }).pipe(Effect.orDie),
);

export default GroupImpl.make(databaseSchema, notes).pipe(
  Layer.provide(list),
  GroupImpl.finalize,
);
GroupImpl.finalize is the per-group completeness check: it only typechecks once every function declared by the spec has been provided to the group layer.
Convex bundles a deployment into a single artifact, but a function’s cold start only evaluates the module graph reachable from its entry point. Confect emits one Convex module per group, so cold-starting a function only evaluates its own group’s spec, impl, and the tables it touches. To keep that cold start fast, import Effect from its submodule paths (import * as Schema from "effect/Schema") rather than the effect barrel in your confect/ files—a barrel import pulls the whole Schema namespace into the module graph your function evaluates at cold start, even when you use only a small part of it.