Skip to content

API reference

The backend entrypoint is backend/src/index.ts. Routes are mounted under /api plus an unauthenticated health route at /healthz.

EnvironmentURL
Developmenthttp://localhost:8787
ProductionYour deployed Worker URL

Protected routes require a valid session. For iOS full-stack flows, requests use:

Authorization: Bearer <session-token>

In the current middleware (backend/src/middleware/auth.ts), any request that includes x-test-user-id is treated as authenticated and receives a synthetic session/user payload.

Returns server status. No authentication required.

Response:

{
"status": "ok",
"environment": "development",
"timestamp": "2026-03-01T12:00:00.000Z"
}

Custom Apple sign-in endpoint (backend/src/routes/auth.ts).

Request body:

{
"token": "eyJ..."
}

Also supports optional identityToken, nonce, and appleUserId.

Success response:

{
"user": { "id": "uuid", "email": "user@example.com", "name": "Jane" },
"token": "session-token",
"requestId": "cf-ray-id"
}

Get the current authenticated session.

Headers: Authorization: Bearer <token>

Response:

{
"user": { "id": "uuid", "email": "...", "name": "..." },
"session": { "id": "uuid", "expiresAt": "..." },
"requestId": "cf-ray-id"
}

End the current session.

Headers: Authorization: Bearer <token>

Response:

{
"success": true,
"requestId": "cf-ray-id"
}

Pass-through to better-auth (GET|POST|PUT|PATCH|DELETE|OPTIONS). This covers routes such as email sign-in/sign-up depending on your better-auth configuration.


These routes are mounted via app.route("/api/user", ...) and require auth.

Returns the current profile:

Response:

{
"data": {
"id": "uuid",
"email": "user@example.com",
"name": "Jane",
"image": null
}
}

Updates profile fields:

{
"name": "Jane Doe",
"image": "https://example.com/avatar.png"
}

Returns current settings:

Response:

{
"data": {
"theme": "light",
"language": "en",
"notifications": true,
"analytics": true
}
}

Updates any subset of:

{
"theme": "dark",
"language": "en",
"notifications": true,
"analytics": true
}

Mounted via app.route("/api", apiRoutes) and auth-protected unless noted.

Service health + D1 connectivity details.

Returns service metadata, feature flags, and supported providers.

Returns current session + user payload from middleware.

List users (limit, offset query params).

Get a user by ID.

Create a user.

Request body:

{
"email": "user@example.com"
}

Update one or more of email, name, image.

Delete a user. Returns 204 No Content.


Requires auth. Persists to the feedback table.

Request body:

{
"category": "bug",
"text": "Detailed feedback message",
"email": "optional@example.com",
"device_info": "optional string or object"
}

Response (201):

{ "success": true }

Mounted via app.route("/api/chat", chatRoutes) and require auth.

Creates a conversation, stores the user message, then streams assistant output.

Request body:

{
"provider": "workers-ai",
"model": "@cf/meta/llama-4-scout-17b-16e-instruct",
"message": "Explain async/await in Swift",
"systemPrompt": null
}

SSE event types from this route:

event: conversation
event: token
event: done
event: error

Lists conversation summaries (limit, offset).

Returns one conversation and full message history.

Appends a message to an existing conversation and streams assistant output.

Deletes a conversation. Returns 204 No Content.


Direct provider proxy without conversation persistence.

Request body:

{
"provider": "openai",
"model": "gpt-5-mini",
"messages": [
{ "role": "system", "content": "You are a helpful assistant." },
{ "role": "user", "content": "Hello!" }
],
"stream": true
}

Supported providers (from backend/src/services/providers/types.ts):

ProviderAPI key env
openaiOPENAI_API_KEY
anthropicANTHROPIC_API_KEY
geminiGEMINI_API_KEY
workers-aiUses Cloudflare AI binding (AI)

When stream: true, response is streamed (SSE/pass-through).


Rate limiting middleware is applied to /api/ai/*.

Storage uses RATE_LIMIT_KV when that binding is available, and falls back to an in-memory counter store otherwise.

Default limits:

WindowLimitConfigurable via
Per minute60 requestsRATE_LIMIT_REQUESTS_PER_MINUTE
Per day1,000 requestsRATE_LIMIT_REQUESTS_PER_DAY

Response headers (every request):

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 58
X-RateLimit-Reset: 1709312400
X-RateLimit-Day-Limit: 1000
X-RateLimit-Day-Remaining: 997
X-RateLimit-Day-Reset: 1709398800

When rate-limited (429):

{
"error": "Rate limit exceeded",
"code": "RATE_LIMITED",
"requestId": "cf-ray-id"
}

Every response includes:

HeaderDescription
X-Request-IdUnique request ID (uses cf-ray or UUID)
Access-Control-Allow-OriginCORS origin
Access-Control-Allow-Credentialstrue

All errors follow a consistent format:

{
"error": "Human-readable error message",
"code": "VALIDATION_ERROR",
"requestId": "cf-ray-id"
}

Common status codes:

CodeMeaning
400Bad request (missing or invalid fields)
401Unauthorized (missing or invalid auth)
404Resource not found
429Rate limit exceeded
503Service unavailable (missing API key config)