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.

Confect uses a spec-impl model that separates function specifications from their implementations. This architectural pattern provides type safety, clear contracts, and better code organization.

What is the Spec-Impl Model?

The spec-impl model divides your code into two layers:

Specification (Spec)

Defines the API contract - function name, arguments, return type, and visibility

Implementation (Impl)

Contains the business logic that fulfills the contract

Why Separate Specs from Implementations?

Type Safety

The CLI generates type-safe bindings from your specs, ensuring that:
  • Function calls match the declared arguments
  • Return types are correctly inferred
  • Schema validation happens automatically

Clear Contracts

Specs serve as documentation and contracts:
  • Anyone can understand your API by reading specs
  • Changes to specs require updating implementations
  • Clients depend only on specs, not implementations

Testability

Separate specs enable:
  • Mocking implementations for testing
  • Contract testing to verify implementations match specs
  • Changing implementations without breaking tests

Code Generation

Specs enable automatic generation of:
  • Type-safe API objects
  • Client-side hooks (React)
  • Function references for scheduling
  • OpenAPI documentation

Function Specifications

Creating a FunctionSpec

Use FunctionSpec to define your function’s contract:
spec/notes/create.ts
import { FunctionSpec, GenericId } from "@confect/core";
import { Schema } from "effect";

export const create = FunctionSpec.publicMutation({
  name: "create",
  args: Schema.Struct({
    text: Schema.String,
    tags: Schema.Array(Schema.String),
  }),
  returns: GenericId.GenericId("notes"),
});

FunctionSpec Properties

Every FunctionSpec includes:
Type: stringThe function name, which must be a valid identifier (alphanumeric and underscores).
name: "create"
Type: Schema.Schema.AnyNoContextAn Effect schema defining the function’s input parameters.
args: Schema.Struct({
  text: Schema.String,
  userId: Schema.optionalWith(Schema.String, { exact: true }),
})
Type: Schema.Schema.AnyNoContextAn Effect schema defining the function’s return type.
returns: Schema.Array(Notes.Doc)

Function Types and Visibility

Confect provides six function spec constructors:
// Queries (read-only)
FunctionSpec.publicQuery({ name, args, returns })
FunctionSpec.internalQuery({ name, args, returns })

// Mutations (read-write)
FunctionSpec.publicMutation({ name, args, returns })
FunctionSpec.internalMutation({ name, args, returns })

// Actions (with external API access)
FunctionSpec.publicAction({ name, args, returns })
FunctionSpec.internalAction({ name, args, returns })
Public functions can be called from clients. Internal functions can only be called from other server-side functions.

Node Actions

For actions that need Node.js APIs:
FunctionSpec.publicNodeAction({ name, args, returns })
FunctionSpec.internalNodeAction({ name, args, returns })
Node actions run in a Node.js environment and have access to Node APIs, but they don’t have access to the Convex database.

Group Specifications

Creating a GroupSpec

Use GroupSpec to organize related functions:
spec/notes.ts
import { GroupSpec } from "@confect/core";
import { create } from "./notes/create";
import { list } from "./notes/list";
import { update } from "./notes/update";

export const notes = GroupSpec.make("notes")
  .addFunction(create)
  .addFunction(list)
  .addFunction(update);

Nested Groups

Groups can contain other groups:
spec/admin.ts
import { GroupSpec } from "@confect/core";
import { users } from "./admin/users";
import { settings } from "./admin/settings";

export const admin = GroupSpec.make("admin")
  .addGroup(users)      // admin.users.*
  .addGroup(settings);   // admin.settings.*

Root Spec

Combine all groups in a root spec:
spec.ts
import { Spec } from "@confect/core";
import { notes } from "./spec/notes";
import { users } from "./spec/users";
import { admin } from "./spec/admin";

export default Spec.make()
  .add(notes)
  .add(users)
  .add(admin);

Function Implementations

Creating a FunctionImpl

Implementations use FunctionImpl.make to connect specs to business logic:
impl/notes/create.ts
import { FunctionImpl } from "@confect/server";
import { Effect } from "effect";
import api from "../../_generated/api";
import { DatabaseWriter } from "../../_generated/services";

export const create = FunctionImpl.make(
  api,
  "notes",
  "create",
  ({ text, tags }) =>
    Effect.gen(function* () {
      const writer = yield* DatabaseWriter;
      
      const noteId = yield* writer.table("notes").insert({
        text,
        tags,
        createdAt: Date.now(),
      });
      
      return noteId;
    }).pipe(Effect.orDie),
);

FunctionImpl.make Parameters

Type: Api.AnyWithPropsThe generated API object from confect/_generated/api.ts.
import api from "../../_generated/api";
Type: stringThe dot-separated path to the group containing this function.
"notes"           // Top-level group
"admin.users"     // Nested group
Type: stringThe function name, matching the spec.
"create"
Type: (args) => Effect<R, E, Returns>The function handler that implements the business logic.
({ text }) => Effect.gen(function* () {
  // Implementation
})

Handler Requirements

Handlers must:
  1. Accept arguments matching the spec’s args schema
  2. Return an Effect that yields a value matching the returns schema
  3. Handle errors appropriately (usually with Effect.orDie)
const handler = ({ text }: { text: string }) =>
  Effect.gen(function* () {
    // Access services
    const writer = yield* DatabaseWriter;
    const reader = yield* DatabaseReader;
    
    // Perform operations
    const id = yield* writer.table("notes").insert({ text });
    
    // Return value matching spec
    return id;
  }).pipe(Effect.orDie);

Group Implementations

Creating a GroupImpl

Group implementations combine function implementations:
impl/notes.ts
import { GroupImpl } from "@confect/server";
import { Layer } from "effect";
import api from "../_generated/api";
import { create } from "./notes/create";
import { list } from "./notes/list";
import { update } from "./notes/update";

export const notes = GroupImpl.make(api, "notes").pipe(
  Layer.provide(create),
  Layer.provide(list),
  Layer.provide(update),
);

Root Implementation

Combine all group implementations:
impl.ts
import { Impl } from "@confect/server";
import { Layer } from "effect";
import api from "./_generated/api";
import { notes } from "./impl/notes";
import { users } from "./impl/users";

export default Impl.make(api).pipe(
  Layer.provide(Layer.mergeAll(notes, users)),
  Impl.finalize,
);
Always call Impl.finalize as the last step to complete the implementation.

Type Safety Benefits

Compile-Time Validation

The TypeScript compiler ensures:
// ✅ Correct usage
const create = FunctionImpl.make(api, "notes", "create", ({ text }) => {
  // text is typed as string
  return Effect.succeed("id_123");
});

// ❌ Type error: missing required argument
const create = FunctionImpl.make(api, "notes", "create", () => {
  //                                                      ^
  // Error: Handler must accept { text: string }
});

// ❌ Type error: wrong return type
const create = FunctionImpl.make(api, "notes", "create", ({ text }) => {
  return Effect.succeed(123); // Error: must return GenericId<"notes">
});

Runtime Validation

Effect schemas provide runtime validation:
// Spec defines validation rules
args: Schema.Struct({
  text: Schema.String.pipe(Schema.minLength(1)),
  priority: Schema.Literal("low", "medium", "high"),
})

// Confect automatically validates at runtime
// Invalid calls are rejected before reaching your handler

Benefits Summary

Type Safety

Compile-time and runtime type checking

Documentation

Specs serve as living documentation

Code Generation

Auto-generate client code and APIs

Testability

Mock implementations for testing

Refactoring

Safe refactoring with type checking

Separation

Clear separation of concerns

Example: Complete Function

Here’s a complete example showing spec and implementation:
spec/notes/update.ts
import { FunctionSpec, GenericId } from "@confect/core";
import { Schema } from "effect";

export const update = FunctionSpec.publicMutation({
  name: "update",
  args: Schema.Struct({
    noteId: GenericId.GenericId("notes"),
    text: Schema.String,
  }),
  returns: Schema.Null,
});
impl/notes/update.ts
import { FunctionImpl } from "@confect/server";
import { Effect } from "effect";
import api from "../../_generated/api";
import { DatabaseWriter } from "../../_generated/services";

export const update = FunctionImpl.make(
  api,
  "notes",
  "update",
  ({ noteId, text }) =>
    Effect.gen(function* () {
      const writer = yield* DatabaseWriter;
      
      yield* writer.table("notes").patch(noteId, { text });
      
      return null;
    }).pipe(Effect.orDie),
);

Next Steps

Services

Learn about Effect services available in Confect

Project Structure

See how to organize specs and implementations

Build docs developers (and LLMs) love