Setup
Arkos provides a JWT-based authentication and authorization system with Role-Based Access Control (RBAC) that secures your auto-generated Prisma model routes, built-in auth routes, file upload routes, and custom ArkosRouter routes out of the box. This page covers the general authentication setup shared across all permission modes and helps you understand how Arkos handles permissions.
Permission Modes
Once a user is authenticated, Arkos needs to know what they're allowed to do. It handles this through two permission modes — Static and Dynamic.
Static
Static mode defines permissions in code via ArkosPolicy (v1.6+) or .auth.ts files. Roles are assigned directly on the User model as an enum field. Use Static when your roles are stable and known at deploy time — most apps start here.
Dynamic
Dynamic mode stores permissions in the database via AuthRole, AuthPermission, and UserRole models. Roles and permissions can be created, updated, and assigned at runtime without a redeploy. Use Dynamic when permissions need to be managed at runtime — multi-tenant apps, SaaS platforms, or any system where roles change frequently.
| Static | Dynamic | |
|---|---|---|
| Permissions defined in | Code | Database |
| Role changes require | Redeploy | Database update |
| Best for | Stable, predictable roles | Runtime-configurable permissions |
| FGAC support | ✅ | ✅ |
Both modes share the same config, user model, auth endpoints, and ArkosPolicy API — the only difference is where permissions are enforced.
Configuration
Set JWT settings via environment variables — the recommended approach:
JWT_SECRET=your-super-secret-jwt-key-here
JWT_EXPIRES_IN=30d
JWT_COOKIE_SECURE=true
JWT_COOKIE_HTTP_ONLY=true
JWT_COOKIE_SAME_SITE=noneArkos picks these up automatically. If you prefer to be explicit, wire them into your config:
import { defineConfig } from "arkos";
export default defineConfig({
authentication: {
mode: "static", // or "dynamic"
login: {
sendAccessTokenThrough: "both",
allowedUsernames: ["username"],
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || "30d",
cookie: {
secure: process.env.JWT_COOKIE_SECURE === "true",
httpOnly: process.env.JWT_COOKIE_HTTP_ONLY !== "false",
sameSite: process.env.JWT_COOKIE_SAME_SITE as "lax" | "strict" | "none",
},
},
},
});import { ArkosConfig } from "arkos";
const arkosConfig: ArkosConfig = {
authentication: {
mode: "static",
login: {
sendAccessTokenThrough: "both",
allowedUsernames: ["username"],
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || "30d",
cookie: {
secure: process.env.JWT_COOKIE_SECURE === "true",
httpOnly: process.env.JWT_COOKIE_HTTP_ONLY !== "false",
sameSite: process.env.JWT_COOKIE_SAME_SITE as "lax" | "strict" | "none",
},
},
},
};
export default arkosConfig;import arkos from "arkos";
arkos.init({
authentication: {
mode: "static",
login: {
sendAccessTokenThrough: "both",
allowedUsernames: ["username"],
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || "30d",
cookie: {
secure: process.env.JWT_COOKIE_SECURE === "true",
httpOnly: process.env.JWT_COOKIE_HTTP_ONLY !== "false",
sameSite: process.env.JWT_COOKIE_SAME_SITE as "lax" | "strict" | "none",
},
},
},
});| Option | Env Variable | Default | Description |
|---|---|---|---|
jwt.secret | JWT_SECRET | — | Signs and verifies tokens — required in production |
jwt.expiresIn | JWT_EXPIRES_IN | "30d" | Token lifetime e.g. "30d", "1h" |
jwt.cookie.secure | JWT_COOKIE_SECURE | true in prod | HTTPS-only cookie |
jwt.cookie.httpOnly | JWT_COOKIE_HTTP_ONLY | true | Blocks JS access to cookie |
jwt.cookie.sameSite | JWT_COOKIE_SAME_SITE | "lax" dev / "none" prod | SameSite cookie policy |
login.sendAccessTokenThrough | — | "both" | "cookie-only" | "response-only" | "both" |
login.allowedUsernames | — | ["username"] | User model fields accepted as login identifiers |
Always set a strong JWT_SECRET in production. Arkos throws on login attempts when no secret is configured.
User Model
Arkos requires a User model with specific fields in your Prisma schema:
// Only needed for Static mode
enum UserRole {
Admin
Editor
User
}
model User {
// Required by Arkos
id String @id @default(uuid())
username String @unique
password String
passwordChangedAt DateTime?
lastLoginAt DateTime?
isSuperUser Boolean @default(false)
isStaff Boolean @default(false)
deletedSelfAccountAt DateTime?
isActive Boolean @default(true)
// Static mode — pick one
role UserRole @default(User) // single role
// roles UserRole[] // multiple roles
// Your own fields
email String? @unique
firstName String?
lastName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}| Field | Purpose |
|---|---|
username | Primary login identifier — customizable via allowedUsernames |
password | Auto-hashed with bcrypt |
passwordChangedAt | Invalidates tokens issued before a password change |
lastLoginAt | Updated on every successful login |
isSuperUser | Bypasses all permission checks — full system access |
isStaff | Frontend-only flag for admin area visibility |
deletedSelfAccountAt | Soft-deletion timestamp |
isActive | When false, blocks all access for that user |
Create at least one user with isSuperUser: true before enabling authentication. By default Arkos requires authentication on all endpoints and only super users have access until permissions are configured.
role / roles is for Static mode only. Dynamic mode replaces it with database-driven role relations.
Protecting Routes
Before diving into permission modes, any route can be protected with authentication: true — this simply requires the user to be logged in, regardless of their role or permissions. For role-based access control, see Static Mode or Dynamic Mode.
import { ArkosRouter } from "arkos";
import postController from "@/src/modules/post/post.controller";
const router = ArkosRouter();
router.get(
{
path: "/api/posts/dashboard",
authentication: true,
},
postController.getDashboard
);
export default router;import { ArkosRouter, RouteHook } from "arkos";
export const hook: RouteHook = {
findMany: { authentication: false }, // public
createOne: { authentication: true }, // login required
updateOne: { authentication: true },
deleteOne: { authentication: true },
};
const router = ArkosRouter();
export default router;RouteHook is the new name for export const config: RouterConfig. If you have existing code using the old name it still works but will log a deprecation warning. See Route Hook for full details.
For role-based or permission-based access control, pick a permission mode:
→ Static Mode — define permissions in code via ArkosPolicy or .auth.ts files
→ Dynamic Mode — manage permissions at runtime via database