<!-- Markdown mirror of https://justscale.sh/docs/fundamentals/guards -->

# Guards

Access control with guards and model permissions

Guards gate access to routes. They run after middleware and can deny the request. JustScale supports inline guard functions, DI-injected guards, and declarative model permissions via `Ticket.can.resolve`.

## Model Permissions

The most common guard pattern uses model-level permissions declared with `permit()`. The model defines who can do what:

src/domain/ticket.tsTypeScript

```typescript
import { defineModel, field } from '@justscale/core/models';
import { permit } from '@justscale/permission';
import { Customer } from './customer';
import { Agent } from './agent';

export class Ticket extends defineModel({
  fields: {
    subject: field.string(),
    status: field.enum('TicketStatus', ['open', 'in_progress', 'resolved', 'closed'] as const),
    customer: field.ref(Customer),
    assignedAgent: field.ref(Agent).optional(),
  },
  permissions: ({ customer }) => ({
    // Customers see their own tickets, agents see all
    view:    [permit(Customer).when(customer), permit(Agent).always()],
    // Only agents can assign and resolve
    assign:  permit(Agent).always(),
    resolve: permit(Agent).always(),
    // Only the owning customer can close
    close:   permit(Customer).when(customer),
  }),
}) {}
```

Then use `Ticket.can.resolve` as a guard on routes:

src/controllers/guarded-routes.tsTypeScript

```typescript
import { createController } from '@justscale/core';
import { Post } from '@justscale/http';
import { auth } from '@justscale/auth';
import { Ticket } from '../domain/models';
import { HelpdeskService } from '../services/helpdesk';

export const TicketController = createController('/tickets', {
  inject: { helpdesk: HelpdeskService },
  routes: ({ helpdesk }) => ({
    // Only agents can resolve tickets
    resolve: Post('/:ticket/resolve')
      .types({ Ticket })
      .use(auth)
      .guard(Ticket.can.resolve)   // 403 if not an agent
      .handle(async ({ params, user, res }) => {
        const ticket = await params.ticket;
        await helpdesk.resolve(ticket, user.name);
        res.status(204).end();
      }),

    // Only the owning customer can close
    close: Post('/:ticket/close')
      .types({ Ticket })
      .use(auth)
      .guard(Ticket.can.close)     // 403 if not the ticket's customer
      .handle(async ({ params, res }) => {
        const ticket = await params.ticket;
        await helpdesk.close(ticket);
        res.status(204).end();
      }),
  }),
});
```

ℹ️Info

`Ticket.can.close` with `permit(Customer).when(customer)` checks that the authenticated user is the customer who created the ticket. The framework resolves the principal, loads the ticket, and compares the reference — all automatically.

## Inline Guards

For simple checks, use an inline function. It receives the accumulated context:

inline-guard.tsTypeScript

```typescript
import { Post } from '@justscale/http';
import { auth } from '@justscale/auth';

// Only users with @support.com emails
Post('/tickets/:ticket/assign')
  .use(auth)
  .guard(({ user, stop }) => {
    if (!user.email.endsWith('@support.com')) {
      return stop();  // Returns 403 Forbidden
    }
  })
  .handle(async ({ params, res, user }) => {
    // Only reached if guard passed
    res.status(204).end();
  });
```

## Combining Guards

Chain multiple `.guard()` calls — all must pass. They execute in order:

src/controllers/combined-guards.tsTypeScript

```typescript
import { Post } from '@justscale/http';
import { auth } from '@justscale/auth';
import { Ticket } from '../domain/models';

Post('/:ticket/resolve')
  .types({ Ticket })
  .use(auth)
  .guard(Ticket.can.resolve)               // 1. Must be an agent
  .guard(async ({ params }) => {            // 2. Ticket must be open
    const ticket = await params.ticket;
    if (ticket?.status === 'closed') throw new Error('Ticket already closed');
  })
  .handle(async ({ params, res, user }) => {
    // Both guards passed
    res.status(204).end();
  });
```

## OR Guards (Array)

Pass an array of guards for OR semantics — any matching guard allows access:

src/controllers/or-guard.tsTypeScript

```typescript
import { Get } from '@justscale/http';
import { auth } from '@justscale/auth';
import { Ticket } from '../domain/models';

// Customer who owns the ticket OR any agent
Get('/:ticket')
  .types({ Ticket })
  .use(auth)
  .guard(Ticket.can.view)  // Array permission — either rule matches
  .handle(async ({ params, res }) => {
    const ticket = await params.ticket;
    res.json(ticket);
  });
```

## Best Practices

- Use model permissions — declare permit() rules on the model, use Model.can.action as guards. One source of truth.
- Auth before guards — always chain .use(auth) before .guard(). Guards need to know who's asking.
- Guards don't add context — they check and deny. Use .use() for adding data to context.
- Prefer stop() over throw — stop() returns a clean 403. Throwing gives 500 unless you set statusCode.

## Next Steps

- Middleware
- Routes
- Typed Parameters
