> ## Documentation Index
> Fetch the complete documentation index at: https://confect.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Middleware

> Attach reusable policies to functions and groups with typed options, schema-derived equality, provided services, and typed errors.

Middleware is reusable logic that runs around every function in a group—the Confect analog of `convex-helpers` custom functions. A middleware runs per invocation, after arguments are decoded and before the handler. It can provide Effect services to downstream handlers (a `CurrentUser`, say), inspect the decoded arguments, short-circuit with a typed error that surfaces—decoded—at every call site, and run logic after the handler completes.

Like functions, middleware is split into a [spec and impl](/v10/concepts/spec-impl-model): the spec half declares the middleware's client-safe interface (its name, the service it provides, how it can fail), while the impl half holds the server-only logic (database lookups, identity resolution). Because the interface lives in the spec, a middleware's errors join the error unions of the functions it covers, and clients decode them with no extra wiring.

## Where middleware lives

Middleware goes in `confect/middleware/`, one middleware per pair of modules, named after the middleware itself:

```
confect/
  middleware/
    RequireUser.spec.ts   ← the declaration: client-safe
    RequireUser.impl.ts   ← the implementation: server-only
  notes.spec.ts
  notes.impl.ts
  tables/
```

`confect/middleware/` is reserved: Confect does not scan it for function groups, so the `*.spec.ts`/`*.impl.ts` pair there means "middleware" rather than "group". A middleware is shared, so it belongs to no single group—putting it in a group's spec would force unrelated groups to import from it.

<Warning>
  Keep implementations out of `*.spec.ts` modules. Every spec is reachable from
  `_generated/refs.ts`, which your client imports, so a spec's entire import
  graph is bundled into the browser—including any middleware implementation
  co-located with its declaration, and everything it closes over (table names,
  index names, your authorization logic). `confect codegen` fails when a module
  a spec reaches value-imports `@confect/server`; when a spec needs a server
  type, reach for it with `import type`, which costs the client nothing.
</Warning>

## Declaring a middleware

Declare a middleware with `MiddlewareSpec.MiddlewareSpec`. The `Config` type parameter has two optional slots: `provides` names the service the middleware provides to handlers, and `requires` names services it consumes from earlier middleware. Both are type-level only; runtime tags are passed to the impl separately. The optional `error` schema declares how the middleware can fail, lazily like a function spec's `error`. To accept attachment options, declare an `options` schema in the constructor as described in [Parameterizing a policy](#parameterizing-a-policy).

```ts confect/middleware/RequireUser.spec.ts theme={null}
import { MiddlewareSpec } from "@confect/core";
import * as Context from "effect/Context";
import * as Schema from "effect/Schema";

import type users from "../_generated/tables/users";

export class CurrentUser extends Context.Service<
  CurrentUser,
  { readonly user: typeof users.Doc.Type }
>()("confect/middleware/RequireUser.spec/CurrentUser") {}

export class NotSignedIn extends Schema.TaggedError<NotSignedIn>()(
  "NotSignedIn",
  {},
) {}

export default class RequireUser extends MiddlewareSpec.MiddlewareSpec<
  RequireUser,
  { provides: CurrentUser }
>()("RequireUser", {
  error: () => NotSignedIn,
  functionTypes: { query: true, mutation: true, action: false },
}) {}
```

Default-export the middleware and name the errors and services it provides as named exports, so both halves of the pair—and the groups that attach it—import it the same way.

Both `provides` and `error` are optional: a middleware may only observe (logging, timing), only guard (short-circuit without providing anything), or both provide and fail.

### Function types

A middleware declares which function types it may cover with the required `functionTypes` option—a boolean flag for each of `query`, `mutation`, and `action` (Node actions count as `action`). Every flag must be specified, so each spec states its coverage outright, the way Convex itself keeps the three function types explicit and separate. The flags must be literal `true` or `false`—they determine the declared function types at the type level, so a computed `boolean` is rejected with a type error, as is declaring all three `false` (a middleware attachable to nothing). Attaching a middleware to a group is a type error unless every function's type is among the middleware's declared function types, so the `RequireUser` above (`action: false`) only fits groups of queries and mutations, and a mutation-only middleware only fits all-mutation groups.

The declared function types also determine which services the middleware's implementation may use—see [below](#services-available-to-an-implementation).

## Attaching to a group

Attach middleware in the group spec with `GroupSpec.middleware`. Attachment is declarative and order-independent with respect to `addFunction`: the middleware covers every function the group declares, whether added before or after the call.

```ts confect/notes.spec.ts theme={null}
import { FunctionSpec, GroupSpec } from "@confect/core";
import * as Schema from "effect/Schema";

import { Id } from "./_generated/id";
import RequireUser from "./middleware/RequireUser.spec";

export default GroupSpec.make()
  .middleware(RequireUser)
  .addFunction(
    FunctionSpec.publicMutation({
      name: "create",
      args: () => ({ text: Schema.String }),
      returns: () => Id("notes"),
    }),
  );
```

Several attachments are rejected at the type level (and at runtime), each with a message naming the problem:

* **Duplicates without options**—attaching the same middleware key without an options schema to a group twice. Middleware with options can be repeated with non-equivalent values; see [Repeating a policy](#repeating-a-policy).
* **Uncovered function types**—attaching a middleware whose `functionTypes` don't include some declared function's type (or adding such a function later).
* **[Plain Convex functions](/v10/server/plain-convex-functions)** of a matching type—their raw handlers pass through Confect untouched, so a middleware could not actually cover them; rejecting the attachment prevents a silent policy hole. A plain Convex function whose type the middleware doesn't declare is fine.

Middleware does not propagate to subgroups: `GroupSpec.middleware` covers only the declaring group's own functions.

When a group attaches more than one middleware, they run in attachment order—the first-attached middleware is outermost. If an earlier middleware short-circuits, later middleware and the handler never run.

## Attaching to a single function

When one function needs a stricter check than its group, attach middleware to the function spec itself with `.middleware()`. Function-level middleware runs after (inside) the group-attached chain, immediately around the handler, and its error joins only that function's error union—the group's other functions are unaffected. The `RequireRole` policy [defined below](#parameterizing-a-policy) restricts a function to permitted user roles:

```ts confect/notes.spec.ts theme={null}
export default GroupSpec.make()
  .middleware(RequireUser)
  .addFunction(
    FunctionSpec.publicMutation({
      name: "deleteAll",
      returns: () => Schema.Null,
    }).middleware(RequireRole, { admin: true }),
  );
```

The same rules apply as at the group level, at the same authoring sites: attaching a middleware whose `functionTypes` don't include the function's type, attaching to a [plain Convex function](/v10/server/plain-convex-functions), or repeating a middleware key without an options schema—including once at each level, in either order—are all type errors. Middleware with options can be repeated within either level or across both; [equivalent options are rejected during validation](#repeating-a-policy). Implementations are provided to the group's impl layer exactly like group-level ones, and `GroupImpl.finalize` demands them just the same.

Note that a failed mutation still rolls back its whole transaction: if a function-level middleware short-circuits after a group middleware has written something, those writes are rolled back with it.

## Parameterizing a policy

Declare a lazy `options` schema in the middleware constructor to reuse one policy with different attachment values. The schema's `Type` determines the options type; `Config` only declares `provides` and `requires`.

A typical use is requiring a signed-in user with one of several permitted roles. Keep the user lookup in `RequireUser`, then let a configurable `RequireRole` consume the `CurrentUser` it provides. For this example, assume the users table has a trusted `role` field defined with `Schema.Literals(["admin", "editor", "viewer"])`:

```ts confect/middleware/RequireRole.spec.ts theme={null}
import { MiddlewareSpec } from "@confect/core";
import * as Schema from "effect/Schema";

import type { CurrentUser } from "./RequireUser.spec";

export class AccessDenied extends Schema.TaggedError<AccessDenied>()(
  "AccessDenied",
  {},
) {}

export default class RequireRole extends MiddlewareSpec.MiddlewareSpec<
  RequireRole,
  { requires: CurrentUser }
>()("RequireRole", {
  options: () =>
    Schema.Struct({
      admin: Schema.optionalKey(Schema.Boolean),
      editor: Schema.optionalKey(Schema.Boolean),
      viewer: Schema.optionalKey(Schema.Boolean),
    }),
  error: () => AccessDenied,
  functionTypes: { query: true, mutation: true, action: false },
}) {}
```

Attach `RequireUser` first so it supplies the loaded user, then enable permitted roles in the second argument to `.middleware()`. One policy now covers both editor-or-admin reads and admin-only writes:

```ts confect/notes.spec.ts theme={null}
import { FunctionSpec, GroupSpec } from "@confect/core";
import * as Schema from "effect/Schema";

import RequireRole from "./middleware/RequireRole.spec";
import RequireUser from "./middleware/RequireUser.spec";

export default GroupSpec.make()
  .middleware(RequireUser)
  .addFunction(
    FunctionSpec.publicQuery({
      name: "list",
      returns: () => Schema.Array(Schema.String),
    }).middleware(RequireRole, { editor: true, admin: true }),
  )
  .addFunction(
    FunctionSpec.publicMutation({
      name: "deleteAll",
      returns: () => Schema.Null,
    }).middleware(RequireRole, { admin: true }),
  );
```

For a group-wide role policy, use `GroupSpec.make().middleware(RequireUser).middleware(RequireRole, { admin: true })`; it covers every function in that group.

The implementation reads the trusted user from `CurrentUser`, not from caller-supplied function arguments:

```ts confect/middleware/RequireRole.impl.ts theme={null}
import { MiddlewareImpl } from "@confect/server";
import * as Effect from "effect/Effect";

import databaseSchema from "../_generated/schema";
import RequireRole, { AccessDenied } from "./RequireRole.spec";
import { CurrentUser } from "./RequireUser.spec";

export default MiddlewareImpl.make(
  databaseSchema,
  RequireRole,
  (effect, { options }) =>
    Effect.gen(function* () {
      const { user } = yield* CurrentUser;

      if (options[user.role] !== true) {
        return yield* new AccessDenied();
      }

      return yield* effect;
    }),
);
```

Provide this implementation once in the group's impl layer, alongside the `RequireUser` implementation and the function implementations. The role flags are specific to each attachment, not to the registered implementation. A user is allowed only when their role's flag is `true`; omitted or `false` flags deny access, and `{}` denies everyone. Missing users fail with `NotSignedIn` from `RequireUser`, while signed-in users outside the permitted roles fail with `AccessDenied`.

The second argument is required and typed from the schema's `Type`. Middleware without an `options` schema still uses `.middleware(Spec)` with no second argument. Options are validated against the schema's **type side**, not its encoded input, during codegen and server registration. Confect does not decode or coerce them: if a schema transforms strings into numbers, pass a number, not a string.

Pass `options` as a schema factory (`() => schema`), not as a schema value.

Both `MiddlewareImpl.make` and `MiddlewareImpl.makeByFunctionType` receive `(effect, { options, invocation })` when the middleware declares an options schema. The typed `options` field contains the attachment value; `invocation` contains `name`, `functionType`, `functionVisibility`, and decoded `args`. Middleware without an options schema receives only `{ invocation }`: `options` is absent from both the context type and the runtime object. A declared schema that accepts `undefined` still receives an `options` field, even when its value is `undefined`. Destructure only what you need: `(effect, { options })` reads the role flags without binding invocation metadata.

The same options mechanism also supports resource argument names, flags, and client-safe resolver functions. Use `MiddlewareImpl.make` rather than the `provides` shorthand when producing a service depends on the attachment options.

The middleware's declared error still determines the client error union, independently of its options. `provides` and `requires` also remain fixed for a spec: an option does not change a handler's service types. For a `tolerateMissing` policy, declare a service containing an `Option` and let the implementation decide whether to provide `None` or fail.

<Warning>
  Options schemas and attachment values are reachable from client refs. Keep
  them and any resolver functions client-safe: do not put secrets, server-only
  imports, or privileged lookup logic in options. Keep authorization and
  database reads in the impl.
</Warning>

### Repeating a policy

You can attach the same middleware spec more than once with non-equivalent options: to a group, to a function, or once at each level. A key must still identify one spec declaration and one registered implementation. Each attachment runs separately, with all group attachments first and then all function attachments, in their respective attachment order:

```ts theme={null}
GroupSpec.make()
  .middleware(RequireUser)
  .middleware(RequireRole, { editor: true, admin: true })
  .addFunction(
    FunctionSpec.publicMutation({
      name: "deleteAll",
      returns: () => Schema.Null,
    }).middleware(RequireRole, { admin: true }),
  );
```

Both role checks must pass before the handler runs, so only an `admin` user can call `deleteAll` in this example. Repeated guards form a conjunction, not a union of allowed policies: two role guards mean the caller must satisfy both, not either. An earlier short-circuit still prevents the remaining attachments and handler from running.

Attachments with the same middleware key must have non-equivalent options according to the options schema. Equivalent options are rejected by `confect codegen` and by server registration, including across the group/function boundary. TypeScript does not check this equivalence. Middleware without an options schema still rejects duplicate keys immediately, both at the type level and at runtime.

Equality follows the options schema rather than evaluating the policy's behavior. In this example, object key order does not matter, but an omitted flag differs from an explicit `false`, even though both deny that role. Use `Schema.overrideToEquivalence` on the options schema if your policy needs custom equality semantics.

If repeated middleware provides the same service tag, the inner provider shadows the outer value for downstream middleware and the handler. Once the nested effect completes, the outer middleware sees its original service context again. Repetition does not change the spec's error union or its `provides` and `requires` type sets, and it does not relax function-type restrictions or propagate middleware to subgroups.

## Depending on another middleware

A middleware can consume a service provided by middleware that runs earlier in the chain. Declare the dependency in the `Config` type parameter's `requires` slot; the implementation's environment then includes it alongside the ctx services. The `RequireRole` example declares `{ requires: CurrentUser }`, so its implementation can yield the user supplied by `RequireUser`.

The spec imports `CurrentUser` with `import type`: it is only named in a type position there, so the import is erased and the declaration stays client-safe. The implementation imports the runtime service tag to read the user.

Satisfaction is checked where the ordering is known:

* Attaching a middleware to a **group** requires its `requires` to be provided by middleware attached to that group earlier—attachment order is chain order, so the check happens right at `GroupSpec.middleware`.
* A **function-level** middleware's `requires` may be satisfied by the group's middleware, which the function spec can't see, so the whole-group check happens at `GroupImpl.make`: every function's middleware must have its `requires` provided by some middleware covering that function.

Like `provides`, `requires` is type-level only, so ordering *within one function's own middleware list* cannot be checked—attach a function-level middleware after its same-level provider, or the missing service surfaces as a defect at runtime.

## Using provided services in handlers

Handlers of covered functions consume the provided service like any other. This is the type-safety contract: a handler requiring `CurrentUser` type-checks exactly when a middleware providing it is attached to the group—remove the `.middleware(RequireUser)` call and the handler below stops compiling.

```ts confect/notes.impl.ts theme={null}
import { FunctionImpl } from "@confect/server";
import * as Effect from "effect/Effect";

import databaseSchema from "./_generated/schema";
import { DatabaseWriter } from "./_generated/services";
import { CurrentUser } from "./middleware/RequireUser.spec";
import notes from "./notes.spec";

const create = FunctionImpl.make(databaseSchema, notes, "create", ({ text }) =>
  Effect.gen(function* () {
    const { user } = yield* CurrentUser;
    const writer = yield* DatabaseWriter;

    return yield* writer.table("notes").insert({ text, userId: user._id });
  }).pipe(Effect.orDie),
);
```

Since handlers depend only on the service, unit tests don't need middleware at all—provide a stub directly with `Effect.provideService(CurrentUser, { user: fakeUser })`.

## Implementing a middleware

A middleware implementation wraps the downstream effect (any remaining middleware plus the handler). Its second argument always contains `invocation`, with the covered function's `name`, `functionType`, `functionVisibility`, and decoded `args`. It also contains `options` when the middleware declares an options schema. The implementation decides whether and how to run the effect:

* **Provide** the declared service to it with `Effect.provideService`—the types require this (or never running the effect at all): the downstream effect's environment carries the provided service as an obligation the implementation must discharge.
* **Short-circuit** by returning `Effect.fail` with the declared error instead of running the effect.
* **Observe** by running the effect and adding logic before or after. The handler's result is opaque to middleware—it can be passed along but not read or replaced—and errors the middleware doesn't declare pass through untouched.

For the common "run something, provide a service" shape, use the `MiddlewareImpl.provides` shorthand, passing the runtime tag for the spec's type-level `provides`:

```ts confect/middleware/RequireUser.impl.ts theme={null}
import { MiddlewareImpl } from "@confect/server";
import * as Effect from "effect/Effect";
import * as Option from "effect/Option";

import databaseSchema from "../_generated/schema";
import { Auth, DatabaseReader } from "../_generated/services";
import RequireUser, { CurrentUser, NotSignedIn } from "./RequireUser.spec";

export default MiddlewareImpl.provides(
  databaseSchema,
  RequireUser,
  CurrentUser,
  Effect.gen(function* () {
    const auth = yield* Auth;
    const reader = yield* DatabaseReader;

    const identity = yield* auth.getUserIdentity.pipe(
      Effect.mapError(() => new NotSignedIn()),
    );
    const user = yield* reader
      .table("users")
      .index("by_token_identifier", (q) =>
        q.eq("tokenIdentifier", identity.tokenIdentifier),
      )
      .first()
      .pipe(Effect.orDie);

    if (Option.isNone(user)) {
      return yield* new NotSignedIn();
    }

    return { user: user.value };
  }),
);
```

The general wrap form takes the downstream effect explicitly—here, actions-only timing middleware that runs code on both sides of the handler:

```ts theme={null}
class Timed extends MiddlewareSpec.MiddlewareSpec<Timed>()("Timed", {
  functionTypes: { query: false, mutation: false, action: true },
}) {}

const TimedImpl = MiddlewareImpl.make(
  databaseSchema,
  Timed,
  (effect, { invocation: { name } }) =>
    Effect.gen(function* () {
      const start = yield* Clock.currentTimeMillis;
      const result = yield* effect;
      const end = yield* Clock.currentTimeMillis;

      yield* Effect.log(`${name} took ${end - start}ms`);

      return result;
    }),
);
```

<Note>
  Convex [freezes time throughout each query or
  mutation](https://docs.convex.dev/functions/runtimes#using-randomness-and-time-in-queries-and-mutations).
  `Clock.currentTimeMillis` reads that frozen timestamp, so this middleware
  would report `0ms` there regardless of the actual duration. Use it only for
  actions. Reading the clock in a query also affects caching, but that is a
  separate concern from measuring elapsed time.
</Note>

### Services available to an implementation

An implementation provided with `MiddlewareImpl.make` uses one strategy for every function type the middleware declares, so its environment is limited to the services available in *all* of those function types:

| Declared function types                    | Services available to `make`                                                           |
| ------------------------------------------ | -------------------------------------------------------------------------------------- |
| One function type                          | That function type's full service set                                                  |
| Queries and mutations                      | `DatabaseReader`, `Auth`, `StorageReader`, `QueryRunner`                               |
| Mutations and actions                      | `Auth`, `Scheduler`, `StorageReader`, `StorageWriter`, `QueryRunner`, `MutationRunner` |
| Any set including both queries and actions | `Auth`, `StorageReader`, `QueryRunner`                                                 |

### Per-function-type implementations

The all-function-types intersection leaves `QueryRunner` as the only database route, but Convex best practices say to [use `ctx.runQuery` sparingly in queries and mutations](https://docs.convex.dev/understanding/best-practices/#use-ctxrunquery-and-ctxrunmutation-sparingly-in-queries-and-mutations). So for database-touching middleware that should also cover actions—say, extending `RequireUser` above to cover all three function types (flipping its `action` flag to `true`)—implement per function type with `MiddlewareImpl.makeByFunctionType` instead: each entry gets that function type's full service set. Read directly in queries and mutations, and call an internal query (defined in a middleware-free group) in actions, where `runQuery` is the only route to the database:

```ts theme={null}
export default MiddlewareImpl.makeByFunctionType(databaseSchema, RequireUser, {
  query: (effect) =>
    Effect.provideServiceEffect(effect, CurrentUser, viaDatabaseReader),
  mutation: (effect) =>
    Effect.provideServiceEffect(effect, CurrentUser, viaDatabaseReader),
  action: (effect) =>
    Effect.provideServiceEffect(effect, CurrentUser, viaRunQuery),
});
```

### Providing to the group layer

Provide the middleware implementation to the group's impl layer like any function implementation. `GroupImpl.finalize` only typechecks once every attached middleware's implementation has been provided, and `confect codegen` reports a missing one by name.

```ts confect/notes.impl.ts theme={null}
import RequireUser from "./middleware/RequireUser.impl";

export default GroupImpl.make(databaseSchema, notes).pipe(
  Layer.provide(create),
  Layer.provide(RequireUser),
  GroupImpl.finalize,
);
```

A middleware implementation is just a layer—share one across groups by providing it to each group's pipeline.

## Middleware errors at call sites

A middleware's `error` schema joins the error union of every function it covers, alongside the function's own `error` schema. Callers consume the union exactly as described in [Error Handling](/v10/server/error-handling)—nothing changes on the client:

```tsx theme={null}
import { QueryResult, useQuery } from "@confect/react";
import refs from "../confect/_generated/refs";

// Error type: NotSignedIn | NoteNotFound
const lookup = useQuery(refs.public.notes.getOrFail, { noteId });

QueryResult.match(lookup, {
  onLoading: () => "Looking up…",
  onSuccess: (note) => note.text,
  onFailure: (error) =>
    error._tag === "NotSignedIn" ? "Sign in first." : "Note not found.",
});
```

Middleware on Confect's [HTTP API](/v10/server/http-api) is separate: HTTP endpoints use Effect's own `HttpApi` middleware machinery.
