Security

Authentication

CellCMS uses JWT (JSON Web Tokens) for user authentication and API tokens for programmatic access. This guide covers the full auth system.

Overview

CellCMS supports two authentication methods:

MethodFormatUse Case
JWT tokenseyJhbG...User sessions (Studio, admin tools)
API tokenscell_...Programmatic access (frontend apps, CI/CD)

Both are passed via the Authorization header:

Authorization: Bearer <token>

JWT Authentication

Login Flow

  1. User sends credentials to the login endpoint
  2. Server returns an access token (short-lived) and a refresh token (long-lived)
  3. Client uses the access token for API requests
  4. When the access token expires, client uses the refresh token to get a new pair

Login

curl -X POST https://api.cellcms.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "admin@cellcms.com", "password": "admin"}'

Response:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "a1b2c3d4e5f6...",
  "user": {
    "id": "uuid",
    "email": "admin@cellcms.com",
    "name": "Admin",
    "role": "admin"
  }
}

Token Lifetimes

TokenDefault LifetimeEnvironment Variable
Access token15 minutesJWT_ACCESS_EXPIRES_IN
Refresh token7 daysJWT_REFRESH_EXPIRES_IN

Access Token Payload

The JWT access token contains:

{
  "sub": "user-uuid",
  "email": "admin@cellcms.com",
  "role": "admin",
  "type": "access",
  "iat": 1700000000,
  "exp": 1700000900
}

Refreshing Tokens

When the access token expires (HTTP 401), use the refresh token to get a new pair:

curl -X POST https://api.cellcms.com/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "a1b2c3d4e5f6..."}'

Response:

{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "new-refresh-token...",
  "user": {
    "id": "uuid",
    "email": "admin@cellcms.com",
    "name": "Admin",
    "role": "admin"
  }
}

Note: Refresh tokens are rotated on each use — the old token is invalidated and a new one is returned. Store the new refresh token for the next refresh.

Logout

Revoke a refresh token to end the session:

curl -X POST https://api.cellcms.com/api/v1/auth/logout \
  -H "Content-Type: application/json" \
  -d '{"refreshToken": "a1b2c3d4e5f6..."}'

Response:

{ "ok": true }

API Tokens

API tokens are designed for programmatic access from frontend applications, CI/CD pipelines, and integrations. They are scoped to a specific project and can be limited to specific permissions and datasets.

Creating API Tokens

Via the API (requires admin JWT):

curl -X POST https://api.cellcms.com/api/v1/tokens/my-project \
  -H "Authorization: Bearer YOUR_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Frontend read-only",
    "permissions": ["read"],
    "dataset": "production"
  }'

Response:

{
  "id": "token-uuid",
  "name": "Frontend read-only",
  "permissions": ["read"],
  "dataset": "production",
  "created_at": "2025-01-15T10:00:00Z",
  "token": "cell_a1b2c3d4e5f6g7h8i9j0..."
}

Important: The raw token (cell_...) is only shown once at creation time. Store it securely — it cannot be retrieved later.

Token Format

API tokens use the prefix cell_ followed by 32 bytes of cryptographically random data encoded as base64url:

cell_dGhpcyBpcyBhIHNhbXBsZSB0b2tlbg...

Tokens are stored as SHA-256 hashes in the database. The plaintext token is never stored.

Permissions

PermissionAllows
readQuery documents, fetch assets, export data
writeCreate, update, delete documents and assets

Common configurations:

Use CasePermissions
Public frontend["read"]
Preview/draft frontend["read"]
CI/CD import pipeline["read", "write"]
Full access["read", "write"]

Dataset Scoping

Tokens can be scoped to a specific dataset. If a dataset is specified, the token can only access that dataset:

{
  "name": "Production reader",
  "permissions": ["read"],
  "dataset": "production"
}

If dataset is omitted or null, the token can access all datasets in the project.

Using API Tokens

Use API tokens exactly like JWT tokens — via the Authorization header:

curl -X POST https://api.cellcms.com/api/v1/data/query/production \
  -H "Authorization: Bearer cell_your-token-here" \
  -H "Content-Type: application/json" \
  -d '{"query": "*[_type == \"post\"]{title, slug}"}'

Listing Tokens

curl https://api.cellcms.com/api/v1/tokens/my-project \
  -H "Authorization: Bearer YOUR_JWT"

Returns all tokens for the project (without the raw token values):

[
  {
    "id": "token-uuid",
    "name": "Frontend read-only",
    "permissions": ["read"],
    "dataset": "production",
    "last_used_at": "2025-01-15T12:00:00Z",
    "created_at": "2025-01-15T10:00:00Z"
  }
]

Revoking Tokens

curl -X DELETE https://api.cellcms.com/api/v1/tokens/my-project/token-uuid \
  -H "Authorization: Bearer YOUR_JWT"

Response:

{ "deleted": true, "id": "token-uuid" }

Roles and Permissions

CellCMS has three roles with a strict hierarchy:

RoleLevelCapabilities
admin3Full access: manage users, projects, tokens, webhooks, all content operations
editor2Create, edit, publish, delete documents and assets
viewer1Read-only: query documents, view assets, export data

Role Hierarchy

Higher roles inherit all permissions of lower roles. An admin can do everything an editor can do, and an editor can do everything a viewer can do.

Endpoint Permissions

Endpoint GroupMinimum Role
Query documentsviewer
Get single documentviewer
View revision historyviewer
Export dataviewer
Create/update documentseditor
Publish/unpublisheditor
Upload/delete assetseditor
Restore revisionseditor
Schedule publishingeditor
Manage usersadmin
Manage projectsadmin
Manage API tokensadmin
Manage webhooksadmin
Import dataadmin

Per-Project Access Control

Users can have different access levels per project. The project_access field on a user record allows fine-grained control:

{
  "project_access": [
    { "projectId": "proj-1", "role": "editor" },
    { "projectId": "proj-2", "role": "viewer" }
  ]
}

When a user accesses a project, CellCMS checks:

  1. If the user has a project-specific role, use that
  2. Otherwise, fall back to their global role

Admins always have full access to all projects regardless of project_access.


Securing Your Frontend

For public-facing websites (Next.js, Gatsby, etc.), use a read-only API token:

import { createClient } from '@cellcms/client'

const client = createClient({
  apiUrl: 'https://your-cellcms-api.com',
  project: 'my-project',
  dataset: 'production',
  token: process.env.CELLCMS_TOKEN, // cell_... read-only token
})

Best practices:

  1. Use read-only tokens for frontend apps — never expose write tokens to the browser
  2. Use environment variables — never hardcode tokens in source code
  3. Scope tokens to a dataset — limit access to only the data needed
  4. Rotate tokens periodically — revoke old tokens and create new ones
  5. Use server-side rendering — keep tokens on the server when possible (Next.js API routes, getServerSideProps)

Server-Side Token Usage (Next.js)

// app/api/posts/route.ts (Next.js App Router)
import { createClient } from '@cellcms/client'

const client = createClient({
  apiUrl: process.env.CELLCMS_API_URL!,
  project: 'my-project',
  dataset: 'production',
  token: process.env.CELLCMS_TOKEN!,
})

export async function GET() {
  const posts = await client.fetch('*[_type == "post"]{title, slug}')
  return Response.json(posts)
}

Rate Limiting

Authentication endpoints are rate-limited to prevent brute-force attacks:

Endpoint GroupRate Limit
Auth routes (/auth/*)10 requests per minute
Protected routes100 requests per minute

Rate limit headers are included in every response:

X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1700000060

When the limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header.


Security Notes

  • JWT secret: Must be a strong, random value in production. Generate with openssl rand -base64 64. The server will refuse to start if the default secret is used in production mode.
  • Password storage: Passwords are hashed with bcrypt (12 salt rounds). Plaintext passwords are never stored.
  • Token storage: API tokens and refresh tokens are stored as SHA-256 hashes. The raw token cannot be recovered from the database.
  • Token rotation: Refresh tokens are rotated on each use, preventing replay attacks.
  • HTTPS: Always use HTTPS in production to protect tokens in transit.