---
title: "Permissions"
description: "RBAC statements, route authority, v-access, and useAccess in a Taman app."
canonical_url: "https://vinicunca.dev/taman/guides/permissions"
---
# Permissions

> RBAC statements, route authority, v-access, and useAccess in a Taman app.

Permissions have two layers: **better-auth access control** (API) and **frontend access** (routes, menus, `v-access`).

## API RBAC (`@taman/rbac`)

`sharedStatements` lists resource/action pairs once so admin and org role sets cannot drift:

```ts
export const sharedStatements = {
  todo: ['create', 'read', 'update', 'delete'],
} as const;
```

### Platform roles (`USER_ROLES`)

| Role | Todo |
| --- | --- |
| `admin` | create, read, update, delete |
| `user` | (none from `sharedStatements`) |

Built in `rbac.admin.ts` with better-auth `admin` plugin statements plus `sharedStatements`.

### Organization roles (`ORGANIZATION_ROLES`)

| Role | Todo |
| --- | --- |
| `owner` | full CRUD |
| `member` | full CRUD (todos are collaborative) |

better-auth's default organization **admin role is omitted**. Passing `roles` **replaces** plugin defaults; a `member.role` value that is not `owner` or `member` fails closed in `resolveMember`.

`CoreService` exposes `can(permission)`. It answers only "does this role allow this action": a platform admin passes on the admin statements, and everyone else needs an organization role that allows it. It does not check row scope.

Each feature service adds its own private `authorize` method on top. `TodoService` shows the pattern: require a membership, check the role with `can`, and return the organization ID that every query must be scoped to:

```ts
private authorize(action: TodoAction): string {
  if (!this.member) {
    throw new ORPCError('FORBIDDEN', {
      message: 'Organization required',
      data: { code: ORG_REQUIRED },
    });
  }

  if (!this.can({ todo: [action] })) {
    throw new ORPCError('FORBIDDEN', { message: `You may not ${action} todos.` });
  }

  return this.member.organizationId;
}
```

Row ownership stays as explicit checks in the service and in the repo's scope filter.

## Route `authority`

On feature routes, `meta.authority` is a list of `TamanRoleName` **or** a callback:

```ts
meta: {
  authority: ['admin'],
  // or: authority: (roles) => roles.includes('admin'),
}
```

`TamanRoleName` is `string` until you augment the registry:

```ts
declare module '@vinicunca/taman-core/typings' {
  interface TamanRoleRegistry {
    role: 'admin' | 'user';
  }
}
```

The web app already maps this to `AuthRoleNames` in `apps/web/src/types/taman-core.d.ts`.

## Access modes

`preferences.app.accessMode` (`frontend` | `backend` | `mixed`), default **frontend**.

`generateAccessible` (`@taman/access`):

- **frontend** — `generateRoutesByFrontend(routes, roles, forbiddenComponent)` using `meta.authority`
- **backend** — `generateRoutesByBackend(options)` via `fetchMenuListAsync`
- **mixed** — merge both by route name (`backend` wins on collision)

`apps/web/src/router/access.ts` still has `// TODO: Implement fetching menu from backend`. Until that exists, **backend/mixed modes cannot load a server menu**. Forbidden access uses `views/_core/fallback/forbidden.vue`. `meta.menuVisibleWithForbidden` keeps the item visible while the page is 403.

## `v-access`

Registered in bootstrap: `registerAccessDirective(app, () => useSessionStore().roles)`.

```vue
<button v-access:role="'admin'">
Admin only
</button>

<button v-access:code="'todo:create'">
If access codes include this
</button>
```

- `v-access:role` in **frontend** mode matches session roles.
- Otherwise (or `v-access:code`) matches `accessStore.accessCodes`.
- Failing elements are **removed** from the DOM on `mounted` (not hidden).

## `useAccess()`

Must run after `registerAccessDirective` (it `inject`s the roles).

```ts
const { hasAccessByRoles, hasAccessByCodes, accessMode, toggleAccessMode } = useAccess();

hasAccessByRoles(['admin']);
hasAccessByCodes(['todo:create']);
```

`toggleAccessMode` flips `frontend` ↔ `backend` in preferences (does not implement a server menu by itself).


## Sitemap

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