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:
| Method | Format | Use Case |
|---|---|---|
| JWT tokens | eyJhbG... | User sessions (Studio, admin tools) |
| API tokens | cell_... | Programmatic access (frontend apps, CI/CD) |
Both are passed via the Authorization header:
Authorization: Bearer <token>
JWT Authentication
Login Flow
- User sends credentials to the login endpoint
- Server returns an access token (short-lived) and a refresh token (long-lived)
- Client uses the access token for API requests
- 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
| Token | Default Lifetime | Environment Variable |
|---|---|---|
| Access token | 15 minutes | JWT_ACCESS_EXPIRES_IN |
| Refresh token | 7 days | JWT_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
| Permission | Allows |
|---|---|
read | Query documents, fetch assets, export data |
write | Create, update, delete documents and assets |
Common configurations:
| Use Case | Permissions |
|---|---|
| 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:
| Role | Level | Capabilities |
|---|---|---|
| admin | 3 | Full access: manage users, projects, tokens, webhooks, all content operations |
| editor | 2 | Create, edit, publish, delete documents and assets |
| viewer | 1 | Read-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 Group | Minimum Role |
|---|---|
| Query documents | viewer |
| Get single document | viewer |
| View revision history | viewer |
| Export data | viewer |
| Create/update documents | editor |
| Publish/unpublish | editor |
| Upload/delete assets | editor |
| Restore revisions | editor |
| Schedule publishing | editor |
| Manage users | admin |
| Manage projects | admin |
| Manage API tokens | admin |
| Manage webhooks | admin |
| Import data | admin |
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:
- If the user has a project-specific role, use that
- 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:
- Use read-only tokens for frontend apps — never expose write tokens to the browser
- Use environment variables — never hardcode tokens in source code
- Scope tokens to a dataset — limit access to only the data needed
- Rotate tokens periodically — revoke old tokens and create new ones
- 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 Group | Rate Limit |
|---|---|
Auth routes (/auth/*) | 10 requests per minute |
| Protected routes | 100 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.
Related Documentation
- API Reference — Full endpoint documentation
- Client SDK — Using tokens with
@cellcms/client - Deployment — Production security configuration