MainSDK
Search⌘K
How-to

Protect a route with permissions

Decide who may do what to which record with access, and refuse the rest with a 403.

Building for 0.3

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will define roles in main.yaml, check a request with access, and see a refusal explained. It takes about 15 minutes.

You need

1. Declare roles

main.yaml
access:
  roles:
    admin: ["*"]
    member: [orders.read]
  owner: [orders.update]

An admin may do anything. A member may read orders. The owner of an order may update it, because its owner equals the subject's id.

2. Check before you act

app/api/orders/[id]/route.ts
import { access, auth } from "mainsdk";

// your own order store
declare function loadOrder(id: string): Promise<{ type: string; id: string; owner: string }>;
declare function saveOrder(order: { id: string }, patch: unknown): Promise<unknown>;

export async function PATCH(req: Request, ctx: { params: Promise<{ id: string }> }) {
  const session = await auth.require(req);
  const order = await loadOrder((await ctx.params).id);   // { type: "order", id, owner, ... }
  const user = { id: session.user, roles: ["member"] };   // roles come from tenancy membership when you use it
  access.require(user, "orders.update", order);   // throws MAIN-ACCESS-0001 (403) unless allowed
  return Response.json(await saveOrder(order, await req.json()));
}

Use await access.check({ subject, action, resource }) when you want the decision and not a throw.

3. Explain a refusal

lib/why.ts
import { access } from "mainsdk";

const user = { id: "u_1", roles: ["member"] };
const order = { type: "order", id: "o_1", owner: "u_2" };

const d = access.decide({ subject: user, action: "orders.update", resource: order });
const why = access.explain(d.id);   // { decision, request }; decision.reasons names the rule

Check it worked

A member who does not own the order gets 403, and d.reasons says no role or owner rule grants orders.update. The owner gets 200, with the reason owner of order may orders.update. Every decision is in the audit log with the policy version.

Next

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet