Skip to content

Authentication

Overview

The Sema Link API uses two layers of authentication:

  1. Cloudflare Access — validates that the request is coming from the Sema Link frontend app (service token headers)
  2. Application auth — validates that the user is logged in (JWT / session token in request headers)

Cloudflare Access (Layer 1)

Every request to the arc subdomain must include:

HeaderValue
CF-Access-Client-IdService token Client ID (from VITE_CF_ACCESS_CLIENT_ID)
CF-Access-Client-SecretService token Client Secret (from VITE_CF_ACCESS_CLIENT_SECRET)

These are injected by the Axios client automatically. See Zero Trust & API Access for setup details.

Application Auth (Layer 2)

After Cloudflare Access validates the request at the edge, the API enforces its own JWT-based authentication for protected routes.

Token Types

TokenLifespanStorage
Access token15 minutesClient memory (not persisted)
Refresh token7 daysStored as SHA-256 hash in refresh_tokens table

The raw refresh token is returned to the client once and never stored on the server — only its hash is kept.

How to Authenticate a Request

Include the access token as a Bearer token in the Authorization header:

Authorization: Bearer <access_token>

Token Lifecycle

  1. Login / Register — the API issues both an access token and a refresh token.
  2. Access token expires — call POST /api/v1/auth/refresh with the refresh token to get a new pair. The old refresh token is revoked immediately (rotation).
  3. Logout — call POST /api/v1/auth/logout with the refresh token. The token is revoked and can no longer be used.

Refresh tokens can only be used once. Attempting to reuse a revoked token returns 401 Unauthorized.

JWT Payload

json
{
  "sub": "<user_id>",
  "accountId": "<account_id>",
  "role": "owner | admin | member"
}

accountId is the currently active organization. role is the user's role within that org, sourced from the account_members junction table — not from users.role.

Role-Based Access

Some endpoints restrict access by role:

RoleCapabilities
ownerFull access — invite members, change roles, remove members
adminCan invite members, cannot change roles or remove members
memberRead/write own profile only

Multi-Organization Support

A single user can belong to multiple organizations. Each organization is a separate accounts row. Membership is tracked in the account_members junction table — one row per (user_id, account_id) pair.

How it works

  • On signup, a personal organization is created automatically and the user gets an account_members row with role: 'owner'.
  • On login, the API picks the user's first active membership and encodes its accountId and role in the JWT.
  • On org creation (POST /api/v1/accounts), a new org is created, the user is added as owner, and the token pair is immediately rotated to the new org context.
  • On org switch (POST /api/v1/accounts/switch), all active refresh tokens for the current org are revoked and a new pair is issued for the target org.

Token rotation on context change

Whenever the active org changes (create or switch), the backend calls revokeAllActiveTokens(userId, currentAccountId) before issuing new tokens. This prevents the old refresh token from being used to re-enter the previous org context without an explicit switch.

Listing memberships

GET /api/v1/accounts returns all orgs the user belongs to, with an isCurrent flag. See Accounts & Orgs endpoints for the full reference.

Internal use only — Sema Link Engineering