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:
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:
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:
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/:idOr 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:
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:
- If a
UserPermissionrow exists for(userId, permissionId), itseffectwins — full stop, regardless of role. - Otherwise, falls back to whatever the user's
AuthRolegrants.
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
- Add
AuthRole,AuthPermission,UserRolemodels to your schema - Replace
role/rolesenum field onUserwithroles UserRole[] - Run
arkos prisma generate - Change
modeto"dynamic"in your config - Create
AuthRolerecords matching your previous enum values - Create
AuthPermissionrecords based on your existing rules - Assign users to roles via
UserRole - Remove
rolesfrom your policy rules — they'll be ignored anyway - Optional: for per-user overrides, add the
UserPermissionmodel andUserPermissionEffectenum to your schema, addpermissions UserPermission[]toUser, then runarkos prisma generate