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