# Adding a permission

> Define a new permission in the Allow catalogue, assign it to roles, and enforce it on endpoints and in the UI.

Permissions in Slicekit are owned by code, not by the database. Every protected endpoint chains
`.RequirePermission(Allow.)`, and the SPA mirrors the same catalogue to hide UI the caller
cannot use. This guide walks the full loop: define the permission, add it to a role catalogue,
enforce it on the API, and gate the matching frontend surface. For the request pipeline that runs
the filter, see [authentication](/docs/authentication); for how the slice itself is built, see
[adding a vertical slice](/docs/vertical-slices).

## The catalogue

`api/src/Slicekit.Core/Permissions/Allow.cs` holds every permission as a strongly-typed
`PermissionDefinition` constant. Naming is `Area.Action` in PascalCase, one permission per action
(`User.GetMe`, `User.CreateApiKey`, `Admin.ListUsers`). Resist bundling read and write into a single
`Manage*` permission: the per-action shape lets API keys be scoped to read-only access.

```csharp
public sealed record PermissionDefinition(string Name, bool IsReadOnly = false)
{
    public static implicit operator string(PermissionDefinition p) => p.Name;
}
```

Two arrays group the catalogue into roles:

- `Allow.UserPermissionCatalog`: granted to every new user on first login.
- `Allow.AdminPermissions`: granted on top, to users flagged `IsAdmin`.

`Allow.RequiredPermissionsFor(user)` returns the set a caller should hold based on their admin flag,
and the sign-in path uses it to backfill missing grants.

## 1. Define the permission

Add a `PermissionDefinition` constant under the matching area block in `Allow.cs`. Pass
`IsReadOnly: true` when the permission only reads state: both permission-picker UIs use that flag to
power their "Read-only" preset.

```csharp
public static readonly PermissionDefinition ProjectList =
    new("Project.List", IsReadOnly: true);

public static readonly PermissionDefinition ProjectCreate =
    new("Project.Create");
```

## 2. Add it to a role catalogue

Append the constant to `UserPermissionCatalog` or `AdminPermissions` in the same file:

```csharp
public static readonly PermissionDefinition[] UserPermissionCatalog =
[
    UserGetMe,
    // ...
    ProjectList,
    ProjectCreate
];
```

On the next API startup, `PermissionSyncService` (a hosted service in `Slicekit.Api`) reconciles the
`Permissions` lookup table to match the catalogue: it inserts the new row and removes any row whose
name no longer appears in code. No EF migration is needed for the catalogue itself, permissions are
owned by code.

`PermissionSyncService` only maintains the lookup table. It never touches `UserPermissions`. Existing
users pick up the new permission on their next successful sign-in, when
`PermissionBackfill.EnsureCatalogPermissionsAsync` diffs their grants against
`Allow.RequiredPermissionsFor(user)` and inserts what is missing. A signed-in session sees the new
claim at the next JWT refresh, or within the two-minute `UserCache` TTL, whichever comes first.

## 3. Enforce it on the endpoint

Chain `.RequirePermission(...)` onto the route in `api/src/Slicekit.Api/Endpoints/v1/`. The filter
rejects the request with a 403 `ForbiddenError` before the handler runs if the caller's claims do not
include the named permission.

```csharp
internal sealed class CreateProjectEndpoint : IEndpoint
{
    public static void Map(IEndpointRouteBuilder routes) =>
        routes.Projects().MapPost("/", HandleAsync)
            .RequirePermission(Allow.ProjectCreate)
            .RequireRateLimiting(RateLimitPolicies.Default)
            .RequireCsrf();
}
```

The filter itself is small. It reads the principal's `permission` claims, which the authentication
middleware projects at sign-in time (both the JWT bearer and the API-key path):

```csharp
internal sealed class PermissionEndpointFilter(PermissionDefinition permission) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(EndpointFilterInvocationContext ctx, EndpointFilterDelegate next)
    {
        if (!ctx.HttpContext.User.HasPermission(permission))
            return new ForbiddenError(permission).ToProblem();
        return await next(ctx);
    }
}
```

Anonymous routes (`/auth/register`, `/auth/login`) chain `.AllowAnonymous()` instead. The
`RequirePermission` filter only runs on authenticated requests.

## 4. Mirror it in the SPA

The SPA exposes `Permission` as a const map in `frontend/src/shared/auth/permissions.ts`. It must
mirror `Allow.cs` exactly: same names, same casing. Add the matching entry whenever you add or rename
a permission server-side.

```tsx

  // ...
  ProjectList: 'Project.List',
  ProjectCreate: 'Project.Create',
} as const;
```

Gate UI with the `usePermissions` hook in `frontend/src/shared/hooks/use-permissions.ts`, which reads
the current user's `permissions` array and exposes a `has(...)` check:

```tsx

function ProjectsToolbar() {
  const { has } = usePermissions();

  return (
    <div>
      {has(Permission.ProjectCreate) && }
    </div>
  );
}
```

The frontend gate is a UX nicety, not a security boundary: the API filter is the real enforcement, so
a hidden button still cannot reach a protected endpoint. See
[frontend permissions](/docs/frontend-permissions) for the consumption pattern in routes and menus.

## 5. Verify

```sh
dotnet build api/slicekit.slnx
```

The build fails if you reference an `Allow.` that does not exist, so a typo in the endpoint surfaces
immediately. Then start the API and hit the protected endpoint without the relevant claim: expect a 403
problem-details response with `"detail": "Missing permission: Project.Create"`. Restart the API and
check the `Permissions` table: the new row appears via `PermissionSyncService` with no migration.

On the frontend:

```sh
cd frontend && pnpm typecheck
```

This passes once the matching `Permission` entry lands, and fails if a `has(Permission.X)` call
references a name you forgot to add.

## Checklist

- [ ] `PermissionDefinition` constant added to `Allow.cs` (`IsReadOnly: true` if read-only).
- [ ] Appended to `UserPermissionCatalog` or `AdminPermissions`.
- [ ] `.RequirePermission(Allow.)` chained on the endpoint.
- [ ] Matching entry added to `frontend/src/shared/auth/permissions.ts`.
- [ ] UI gated with `usePermissions().has(Permission.)` where relevant.
- [ ] `dotnet build` and `pnpm typecheck` pass; lookup row appears after restart.
