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: 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 inconfect/middleware/, one middleware per pair of modules, named after the middleware itself:
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.
Declaring a middleware
Declare a middleware withMiddlewareSpec.MiddlewareSpec. The Config type parameterâs provides slot names the service the middleware provides to handlers â type-level only; the runtime tag is passed to the impl separately. The optional error schema declares how the middleware can fail, lazily like a function specâs error.
confect/middleware/RequireUser.spec.ts
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 requiredfunctionTypes 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.
Attaching to a group
Attach middleware in the group spec withGroupSpec.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.
confect/notes.spec.ts
- Duplicates â attaching the same middleware to a group twice.
- Uncovered function types â attaching a middleware whose
functionTypesdonât include some declared functionâs type (or adding such a function later). - 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.
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. Given a second middleware RequireAdmin, declared like RequireUser above:
confect/notes.spec.ts
functionTypes donât include the functionâs type, attaching to a plain Convex function, or attaching the same middleware twice â including once at each level, in either order â are all type errors. 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.
Depending on another middleware
A middleware can consume a service provided by middleware that runs earlier in the chain. Declare the dependency in theConfig type parameterâs requires slot; the implementationâs environment then includes it alongside the ctx services:
confect/middleware/RequireAdmin.spec.ts
confect/middleware/RequireAdmin.impl.ts
import type in the spec half: CurrentUser is only ever named in a type position there, so the import is erased and the declaration stays client-safe.
Satisfaction is checked where the ordering is known:
- Attaching a middleware to a group requires its
requiresto be provided by middleware attached to that group earlier â attachment order is chain order, so the check happens right atGroupSpec.middleware. - A function-level middlewareâs
requiresmay be satisfied by the groupâs middleware, which the function spec canât see, so the whole-group check happens atGroupImpl.make: every functionâs middleware must have itsrequiresprovided by some middleware covering that function.
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 requiringCurrentUser type-checks exactly when a middleware providing it is attached to the group â remove the .middleware(RequireUser) call and the handler below stops compiling.
confect/notes.impl.ts
Effect.provideService(CurrentUser, { user: fakeUser }).
Implementing a middleware
A middleware implementation wraps the downstream effect (any remaining middleware plus the handler). It receives that effect together with metadata about the invocation â the covered functionâsname, functionType, and functionVisibility, plus its decoded args â and decides whether and how to run it:
- 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.failwith 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.
MiddlewareImpl.provides shorthand, passing the runtime tag for the specâs type-level provides:
confect/middleware/RequireUser.impl.ts
Inside queries, reading the
Clock service opts the query out of Convexâs
cache, exactly as it would in a handler â see
Determinism. A timing middleware is best
reserved for mutations and actions.Services available to an implementation
An implementation provided withMiddlewareImpl.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:
Per-function-type implementations
The all-function-types intersection leavesQueryRunner as the only database route, but Convex best practices say to use ctx.runQuery 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:
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.
confect/notes.impl.ts
Middleware errors at call sites
A middlewareâserror 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 â nothing changes on the client:
HttpApi middleware machinery.