---
title: "Authorization"
description: "Roles, permissions, and workspace membership — setupAuthorization, the pre-authorized createFunctions tiers, RoleBoundary, and hasRole / can on the client."
canonical_url: "https://nuxt-backend.dev/docs/auth/access/roles"
---
# Authorization

> Roles, permissions, and workspace membership — setupAuthorization, the pre-authorized createFunctions tiers, RoleBoundary, and hasRole / can on the client.

Authorization sits on top of the bundled Better Auth setup: **app-wide roles** and **permission statements** from the admin plugin, and **workspace membership** from the organization plugin. Reads are claims-first — `role`, `banned`, `activeOrganizationId` and `impersonatedBy` ride on the Convex JWT, so most checks cost no database read. Claims can lag a role change by up to the JWT lifetime (15 minutes); pass `{ fresh: true }` where that matters.

## Server: `setupAuthorization`

The scaffolded `backend/functions.ts` builds the helpers once:

```ts [backend/functions.ts]
import { setupAuthorization } from 'nuxt-backend/authorization'
import { createFunctions } from 'nuxt-backend/functions'
import { components } from './_generated/api'
import { action, internalMutation, mutation, query } from './_generated/server'

export const authorization = setupAuthorization(components, { internalMutation })
export const { setUserRole } = authorization

export const { authed, org, admin, withRole } = createFunctions(
  { query, mutation, action },
  authorization,
)
```

`setupAuthorization(components, options?)` returns:

<field-group>
<field name="getUser(ctx)" type="Promise<AuthorizationUser | null>">

The signed-in user from identity claims — `id`, `email`, `name`, `role` (`'user'` when unset), `banned`, `activeOrganizationId`, `impersonatedBy` (the admin impersonating the user, or `null`), and the raw `claims`. `null` when signed out.

</field>

<field name="requireUser(ctx)">

Throws `unauthenticated` when signed out, and `forbidden` with reason `banned` when banned: [catalog codes](https://nuxt-backend.dev/docs/reference/backend/errors) the client reads with `getBackendError`.

</field>

<field name="requireRole(ctx, role | role[], { fresh? })">

Require an app-wide role (any match passes; Better Auth stores multiple roles comma-separated). `fresh: true` re-reads the user document past JWT staleness.

</field>

<field name="requirePermission(ctx, { resource: [actions] }, { fresh? })">

Require permission statements (e.g. `{ user: ['ban'] }`) against the user's role via the access-control roles — the admin plugin's built-in `admin` / `user` roles by default, or the `roles` map you pass.

</field>

<field name="requireOrganization(ctx)">

Require an active workspace on the session; returns `{ user, organizationId }`.

</field>

<field name="requireMember(ctx, { organizationId?, role? })">

Require workspace membership — **always a fresh member-table read**. Defaults to the active workspace; `role` also requires a workspace role (`owner` / `admin` / `member` or custom).

</field>

<field name="setUserRole" type="internalMutation">

Present when `internalMutation` was supplied: the bootstrap for your first admin.

</field>
</field-group>

Mint the first admin from the CLI (an internal mutation, so never callable from the client):

```bash
npx convex run functions:setUserRole '{"email":"you@example.com","role":"admin"}'
```

### Custom roles and permissions

`nuxt-backend/authorization` re-exports Better Auth's building blocks so you never import its internals: `createAccessControl`, `defaultStatements`, `adminAc`, `userAc`. Define statements once, hand the roles to both the admin plugin (so the client's `can()` knows them) and `setupAuthorization` (so `requirePermission` does):

```ts [backend/roles.ts]
import { adminAc, createAccessControl, defaultStatements } from 'nuxt-backend/authorization'

const statement = { ...defaultStatements, project: ['create', 'archive'] } as const
export const ac = createAccessControl(statement)
export const roles = {
  admin: ac.newRole({ ...adminAc.statements, project: ['create', 'archive'] }),
  editor: ac.newRole({ project: ['create'] }),
}
```

```ts [backend/auth.ts]
setupAuth(components, query, {
  email,
  admin: { ac, roles },          // the admin plugin's own options
})
```

```ts [backend/functions.ts]
export const authorization = setupAuthorization(components, { internalMutation, roles })
```

### Impersonation

An administrator with the `impersonate` permission can act as another account through the admin plugin's `/admin/impersonate-user`. The session that opens carries `impersonatedBy`, which `getUser` and `requireUser` return. The auth routes refuse what would outlast the visit: adding, renaming or removing a passkey, changing the account's email address, deleting it, inviting people to its workspaces or changing their roles, linking another sign-in method, and approving an agent. `/admin/stop-impersonating` always lets the administrator out. Refuse the same in your own functions wherever an administrator must not act for the person: `if (user.impersonatedBy) throw backendError('forbidden', { reason: 'impersonating' })`.

## In your functions

The `authed`, `org`, `admin` and `withRole` builders run these checks for you on every call: see [Protect backend functions](https://nuxt-backend.dev/docs/auth/access/functions).

## Client: roles on `useAuth()`

The same claims are readable in the app, synchronously:

```vue
<script setup lang="ts">
const { role, hasRole, can, banned } = useAuth()
</script>

<template>
  <AdminNav v-if="hasRole('admin')" />
  <BanButton v-if="can({ user: ['ban'] })" />
</template>
```

`can()` evaluates permission statements locally through the admin plugin's `checkRolePermission` — no request — against the roles configured on the plugin.

## `<RoleBoundary>`

Renders its default slot only when the signed-in user has the required app-wide role or permission; `placeholder` while the session loads, `fallback` when the check fails. Banned accounts never pass.

```vue
<template>
  <RoleBoundary role="admin">
    <AdminPanel />
    <template #fallback><p>Admins only.</p></template>
  </RoleBoundary>

  <RoleBoundary :permission="{ user: ['ban'] }">
    <BanButton />
  </RoleBoundary>
</template>
```

The billing counterpart is [`<FeatureBoundary>`](https://nuxt-backend.dev/docs/billing/feature-gating); the workspace counterpart is [`<OrganizationBoundary>`](https://nuxt-backend.dev/docs/auth/workspaces#organizationboundary).

<note>

Client-side gates are for UI. Every function that matters must check on the server too — that is what the tiers above are for.

</note>

<page-keys>

- Try it live in the playground: [/playground/platform/authorization](https://nuxt-backend.dev/playground/platform/authorization)
- API reference: [/docs/reference/api/authorization](https://nuxt-backend.dev/docs/reference/api/authorization)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/authorization.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/authorization.ts)

</page-keys>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
