Core ConceptsAuthenticationPermissions

Dynamic

Dynamic mode is Arkos's database-driven permission system. Roles and permissions live in AuthRole, AuthPermission, and UserRole models and can be created, updated, and assigned at runtime without a redeploy — making it the right choice for multi-tenant apps, SaaS platforms, or any system where roles change frequently.

Before using Dynamic mode make sure you have authentication configured. See Authentication Setup.

User Model

Dynamic mode replaces the role/roles enum field with a UserRole relation, and adds three required models:

prisma/schema.prisma
model User {
  // ... required Arkos fields
  roles       UserRole[]

  // your own fields
  email     String?  @unique
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

model AuthRole {
  id          String           @id @default(uuid())
  name        String           @unique
  permissions AuthPermission[]
  users       UserRole[]
  createdAt   DateTime         @default(now())
  updatedAt   DateTime         @updatedAt
}

model AuthPermission {
  id        String     @id @default(uuid())
  resource  String
  action    String
  roles     AuthRole[]
  createdAt DateTime   @default(now())
  updatedAt DateTime   @updatedAt

  @@unique([resource, action])
}

model UserRole {
  id     String   @id @default(uuid())
  userId String
  roleId String
  user   User     @relation(fields: [userId], references: [id])
  role   AuthRole @relation(fields: [roleId], references: [id])

  @@unique([userId, roleId])
}

Before 1.7.0, AuthPermission had a roleId field and @@unique([resource, action, roleId]) — one row per role. 1.7.0 made permissions shared across roles via a many-to-many roles relation and changed the unique constraint to @@unique([resource, action]). See Migrating from Static if you're upgrading an existing Dynamic-mode project.

See the full required User model at User Model Authentication Setup.

Configuration

Change mode to "dynamic" — everything else stays the same as the base Authentication Setup:

arkos.config.ts
import { defineConfig } from "arkos";

export default defineConfig({
  authentication: {
    mode: "dynamic",
    // ... rest of your config unchanged
  },
});

Defining Dynamic Permissions

Same ArkosPolicy and .auth.ts API as Static mode — the only difference is that roles inside rules are ignored at enforcement time. Actual enforcement comes from database records instead. Define your policy with names and descriptions for discovery, and skip the roles — or keep roles: ["*"] where all authenticated users should have access:

src/modules/post/post.policy.ts
import { ArkosPolicy } from "arkos";

const postPolicy = ArkosPolicy("post")
  .rule("Create", { name: "Create Post", description: "Create new posts" })
  .rule("Update", { name: "Update Post" })
  .rule("Delete", { name: "Delete Post" })
  .rule("View", { roles: ["*"] }); // * still works — all authenticated users

export default postPolicy;

Wiring to routes is identical to Static mode — see Static Mode — Using Permissions in Routes.

Managing Permissions

Arkos auto-generates full CRUD endpoints for AuthRole, AuthPermission, and UserRole:

GET    /api/auth-roles
POST   /api/auth-roles
PATCH  /api/auth-roles/:id
DELETE /api/auth-roles/:id

GET    /api/auth-permissions
POST   /api/auth-permissions
PATCH  /api/auth-permissions/:id
DELETE /api/auth-permissions/:id

GET    /api/user-roles
POST   /api/user-roles
DELETE /api/user-roles/:id

Or manage them programmatically:

const adminRole = await prisma.authRole.create({ data: { name: "Admin" } });
const editorRole = await prisma.authRole.create({ data: { name: "Editor" } });

const createPost = await prisma.authPermission.create({
  data: {
    resource: "post",
    action: "Create",
    roles: { connect: { id: editorRole.id } },
  },
});
const deletePost = await prisma.authPermission.create({
  data: {
    resource: "post",
    action: "Delete",
    roles: { connect: { id: adminRole.id } },
  },
});

await prisma.userRole.create({
  data: { userId: user.id, roleId: adminRole.id },
});

User-Level Permission Overrides

Available from 1.7.0-rc

Sometimes you need to grant or revoke a specific permission for one user without creating a one-off role for them. UserPermission sits on top of role-derived permissions and lets you override the result per user, per permission:

prisma/schema.prisma
model UserPermission {
  id           String               @id @default(uuid())
  createdAt    DateTime             @default(now())
  updatedAt    DateTime             @updatedAt
  effect       UserPermissionEffect @default(Allow)
  userId       String
  user         User                 @relation(fields: [userId], references: [id])
  permissionId String
  permission   AuthPermission       @relation(fields: [permissionId], references: [id])

  @@unique([userId, permissionId])
}

enum UserPermissionEffect {
  Allow
  Deny
}

// Update the user to add new relation
model User {
  // ... everything else remains the same
  permissions UserPermission[]
}


// Update it to add the new relation
model AuthPermission {
  // everything else remains the same
  users     UserPermission[]
}

Resolution order — this is exactly how checkDynamicAccessControl decides access:

  1. If a UserPermission row exists for (userId, permissionId), its effect wins — full stop, regardless of role.
  2. Otherwise, falls back to whatever the user's AuthRole grants.

If your Prisma client has no userPermissions delegate — i.e. you haven't added the model yet — Arkos skips the override lookup entirely and falls back to role-derived permission. No error, no behavior change. Safe to leave out until you actually need per-user overrides.

Already on Dynamic mode and want user-level overrides on an existing project? See Adding User Permissions in Old Projects for the schema changes and migration steps.

Imperative Checks

ArkosPolicy can* methods work in Dynamic mode too, checking against database permissions. See Fine-Grained Access Control for full usage.

Imperative checks in fine-grained access control also work with auth config files. See the full guide at Fine-Grained Access Control.

Migrating from Static

  1. Add AuthRole, AuthPermission, UserRole models to your schema
  2. Replace role/roles enum field on User with roles UserRole[]
  3. Run arkos prisma generate
  4. Change mode to "dynamic" in your config
  5. Create AuthRole records matching your previous enum values
  6. Create AuthPermission records based on your existing rules
  7. Assign users to roles via UserRole
  8. Remove roles from your policy rules — they'll be ignored anyway
  9. Optional: for per-user overrides, add the UserPermission model and UserPermissionEffect enum to your schema, add permissions UserPermission[] to User, then run arkos prisma generate