Skip to content

Authentication ​

The control plane supports two authentication mechanisms: JWT-based session auth for the dashboard and API token auth for client APIs.

Authentication Modes ​

ModeUsed ForMechanism
JWT SessionDashboard UIHTTP-only cookie
API TokenClient APIs (/api/client/v1/*)Authorization: Bearer <token>

JWT Session Authentication ​

Login Flow ​

  1. User submits slug + email + password to POST /api/auth/login
  2. Server finds tenant by slug, switches to tenant database
  3. Validates email/password against stored bcrypt hash
  4. Generates JWT with user info, license features, and tenant data
  5. Sets HTTP-only cookie (token)
  6. Returns user profile

Registration Flow ​

  1. User submits company name, slug, name, email, password to POST /api/auth/register
  2. Server creates tenant with the given slug
  3. Creates a new tenant_{slug} database
  4. Creates user as owner with hashed password
  5. Assigns default license (FREE)
  6. Generates JWT and sets cookie
  7. Sends welcome email

JWT Payload ​

typescript
{
  userId: string;
  email: string;
  role: 'owner' | 'admin' | 'project_admin' | 'user';
  tenantId: string;
  tenantSlug: string;
  tenantDbName: string;
  licenseType: string;
  features: string[];
}

The JWT is signed using the jose library (Edge Runtime compatible) with JWT_SECRET.

Middleware Processing ​

The global middleware (src/middleware.ts) processes every request:

Request → Is public path? → Pass through
        → Is client API? → Skip cookie auth (Bearer handled in route)
        → Extract cookie → Verify JWT → Check license endpoint access
        → Inject headers → Forward to route handler

Headers injected for authenticated requests:

HeaderContent
x-user-idUser ObjectId
x-user-emailUser email
x-user-roleowner, admin, project_admin, user
x-tenant-idTenant ObjectId
x-tenant-slugTenant slug
x-tenant-db-nametenant_{slug}
x-license-typeLicense tier
x-featuresJSON array of feature flags
x-request-idRequest UUID

Public Paths ​

These paths skip authentication:

  • /login, /register
  • /api/auth/*
  • /api/health/*

API Token Authentication ​

For programmatic access, tenants create API tokens through the dashboard. These tokens authenticate requests to /api/client/v1/* endpoints.

Every token is owned by a user record, and that user's canLogin flag shapes where the token gets managed:

  • A regular member (canLogin: true, the default) mints and manages their own tokens under Configure → API Tokens.
  • A Programmatic User (canLogin: false) — a login-disabled identity with no password and an optional email, meant to exist only to own API tokens (a service account, a CI job, an integration) — can never sign in, so it can't reach that self-service page. An owner/admin instead manages its tokens from its row in Configure → Members → <user>, on that user's dedicated per-user token page.

For anything that isn't a human logging into the dashboard, prefer creating a Programmatic User and minting its tokens from that per-user page (or the equivalent Client API calls below) rather than handing out a real teammate's personal token. See User Types (canLogin) for how the flag interacts with roles.

Tokens are managed under Configure → API Tokens (your own tokens) or Configure → Members → <user> (any user's tokens, including a Programmatic User's). Each row shows the label, creation date, and last-used timestamp; the full secret is shown only once at creation time:

API Tokens

Usage ​

bash
curl -X POST https://gateway.example.com/api/client/v1/chat/completions \
  -H "Authorization: Bearer cpeer_abc123..." \
  -H "Content-Type: application/json" \
  -d '{"model": "gpt-4", "messages": [{"role": "user", "content": "Hello"}]}'

Provisioning a non-login identity from the API ​

The per-user token page has a Client API equivalent — two endpoints, both requiring an owner/admin-owned token:

  1. POST /client/v1/users — creates a Programmatic User. canLogin is always forced to false server-side, so this endpoint can never create a login-capable account no matter what the request body sends.
  2. POST /client/v1/users/:id/tokens — mints an API token for that user (or any user in the tenant) and returns the plaintext secret once.
bash
# Create a login-disabled service account
curl -X POST https://gateway.example.com/api/client/v1/users \
  -H "Authorization: Bearer cpeer_admin_token..." \
  -H "Content-Type: application/json" \
  -d '{"name": "billing-sync-bot", "role": "user"}'

# Mint an API token for it (id from the response above)
curl -X POST https://gateway.example.com/api/client/v1/users/<user_id>/tokens \
  -H "Authorization: Bearer cpeer_admin_token..." \
  -H "Content-Type: application/json" \
  -d '{"label": "billing-sync nightly job"}'

See Users API for full request/response fields and error cases.

requireApiToken Helper ​

All client API routes use the requireApiToken helper:

typescript
import { requireApiToken, ApiTokenAuthError } from '@/lib/services/apiTokenAuth';

export async function POST(request: NextRequest) {
  try {
    const ctx = await requireApiToken(request);
    // ctx.token, ctx.tokenRecord, ctx.tenant
    // ctx.tenantId, ctx.tenantSlug, ctx.tenantDbName
    // ctx.projectId, ctx.user
  } catch (error) {
    if (error instanceof ApiTokenAuthError) {
      return NextResponse.json({ error: error.message }, { status: error.status });
    }
    throw error;
  }
}

Token Validation Flow ​

  1. Extract Bearer token from Authorization header
  2. Check cache (SHA-256 hash key, 60s TTL)
  3. On cache miss: query database, resolve tenant
  4. Fire-and-forget: update lastUsed timestamp
  5. Switch to tenant database
  6. Ensure default project exists
  7. Resolve user from token record
  8. Return ApiTokenContext

Token Properties ​

FieldDescription
tokenThe raw token string
userIdOwning user
tenantIdOwning tenant
projectIdScoped project (optional)
expiresAtExpiration date (optional)
lastUsedLast usage timestamp

License-Based Feature Control ​

Features are controlled through a license system defined in src/config/policies.json:

typescript
import { LicenseManager } from '@/lib/license/license-manager';

const hasAccess = LicenseManager.hasFeature(licenseType, 'LLM_CHAT');
const canAccessEndpoint = LicenseManager.hasEndpointAccess(licenseType, '/api/models');

License Tiers ​

TierFeaturesRequest Limit
FREE16 features1,000/month
STARTER10 features10,000/month
PROFESSIONAL14 features100,000/month
ENTERPRISEAll featuresUnlimited
ON_PREMISEAll featuresUnlimited

Feature Endpoint Mapping ​

Each feature in policies.json maps to API endpoint patterns:

json
{
  "LLM_CHAT": {
    "name": "LLM Chat",
    "endpoints": ["/api/chat/*", "/api/client/v1/chat/*"]
  }
}

The middleware checks these mappings automatically.

User Roles ​

RoleScope
ownerFull tenant control
adminManage users and settings
project_adminManage assigned projects
userAccess assigned projects only

User Types (canLogin) ​

role and canLogin are independent axes on the same user record — role governs what a user can do once authenticated, canLogin governs whether it can authenticate as a dashboard session at all. A user of any role can be either type:

canLoginTypeMeaning
true (default — missing/undefined is treated as true for back-compat)Regular memberHas a password, can log into the dashboard and hold a JWT session, in addition to owning API tokens.
falseProgrammatic UserNo password, no dashboard session, email optional. Exists only to own API tokens — a service account, integration, or CI identity. Shown with a "No login" status badge and a "Programmatic" badge next to its role in Configure → Members.

POST /client/v1/users (the Client API) can only ever create canLogin: false users — it forces the flag server-side regardless of the request body, so an API token can never provision a login-capable account. A human teammate must still be added from the dashboard, with a password, via Configure → Members → Add User (with the Send invite toggle left on).

Configuration ​

VariableDefaultDescription
JWT_SECRET—Required. Secret for JWT signing
JWT_EXPIRES_IN7dJWT expiration duration
PROVIDER_ENCRYPTION_SECRETJWT_SECRETEncryption key for stored credentials

Studio · Pulse · Console · Agent SDK and more — the Cognipeer documentation hub