---
title: "Protect a route with permissions"
description: "Decide who may do what to which record with access, and refuse the rest with a 403."
section: "guides"
status: "Building for 0.3"
spec: "access"
canonical: "https://mainsdk.dev/docs/guides/protect-a-route-with-permissions"
updated: "2026-10-28"
---

# Protect a route with permissions

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

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/protect-a-route-with-permissions · 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

- A route that already knows the user (see [Sign-in and sessions](https://mainsdk.dev/docs/interfaces/auth)).

## 1. Declare roles

```yaml title="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

```ts title="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

```ts title="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

- [Permissions](https://mainsdk.dev/docs/interfaces/access)
- [Policy decisions](https://mainsdk.dev/docs/interfaces/policy)
