Skip to main content
In Ship, authorization is composition. You don’t write if (!user) throw inside handlers — you stack gates onto an endpoint with .use(...). Each gate is an oRPC middleware that guarantees something about context (or throws) before the handler runs.

How .use() composes

@/endpoint already carries every global middleware. Per-endpoint, .use(...) adds gates in order. Order matters — a gate can refine the context the next one reads:
resources/users/endpoints/[userId]/update.ts
A gate either calls next({ context }) — optionally narrowing the context type — or throws an ORPCError. Because the narrowing flows through .use(), your handler’s context is correctly typed without a single cast.

The gates

isAuthorized

Requires a session. Stack it when an endpoint needs any signed-in user:
resources/users/endpoints/current.get.ts
Under the hood it’s a thin guard:
middlewares/is-authorized.ts

isAdmin

Requires the user to be an admin. It checks the session first, then user.isAdmin, throwing FORBIDDEN otherwise — the admin user list and other admin endpoints stack this:

canAccess

The general “load it or 404” gate. You pass a load function — the gate runs it, stores the result in context[key], and throws NOT_FOUND if it comes back empty. The loader decides what “exists” means (a custom where, a relationship check, a relation-loaded query):

canEdit

The common ownership case, built on canAccess. Pass the context key and a @ship/db service; it loads the entity by id, scoped to the current user, and throws NOT_FOUND on a mismatch — so a non-owner can’t tell the row exists. Defaults: idKey = `${key}Id` (falling back to input.id), owner column = userId, soft-delete aware:

Per-resource gates

When an ownership rule recurs across a resource’s endpoints, factor it into <resource>/middlewares/can-edit-*.ts and reuse it. The Notes plugin ships exactly this:
resources/notes/middlewares/can-edit-note.ts
resources/notes/endpoints/remove.ts
canEdit runs after .input() — it reads the validated id off input. Stack .use(isAuthorized) before it so context.user is guaranteed.

See also