Core ConceptsAuthentication

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.

StaticDynamic
Permissions defined inCodeDatabase
Role changes requireRedeployDatabase update
Best forStable, predictable rolesRuntime-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:

.env
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=none

Arkos picks these up automatically. If you prefer to be explicit, wire them into your config:

arkos.config.ts
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",
      },
    },
  },
});
arkos.config.ts
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;
src/app.ts
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",
      },
    },
  },
});
OptionEnv VariableDefaultDescription
jwt.secretJWT_SECRETSigns and verifies tokens — required in production
jwt.expiresInJWT_EXPIRES_IN"30d"Token lifetime e.g. "30d", "1h"
jwt.cookie.secureJWT_COOKIE_SECUREtrue in prodHTTPS-only cookie
jwt.cookie.httpOnlyJWT_COOKIE_HTTP_ONLYtrueBlocks JS access to cookie
jwt.cookie.sameSiteJWT_COOKIE_SAME_SITE"lax" dev / "none" prodSameSite 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:

prisma/schema.prisma
// 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
}
FieldPurpose
usernamePrimary login identifier — customizable via allowedUsernames
passwordAuto-hashed with bcrypt
passwordChangedAtInvalidates tokens issued before a password change
lastLoginAtUpdated on every successful login
isSuperUserBypasses all permission checks — full system access
isStaffFrontend-only flag for admin area visibility
deletedSelfAccountAtSoft-deletion timestamp
isActiveWhen 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.

src/modules/post/post.router.ts
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;
src/modules/post/post.router.ts
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