CORS & Data API
Configure Cross-Origin Resource Sharing for the HTTP Data API, manage scoped tokens, and understand the security model for browser-based access.
CORS & Data API
The AxiomDB Data API provides an HTTP interface for querying and mutating data in your Postgres instances. It is designed for browser-based applications, serverless functions, and any client that prefers HTTP over native TCP connections. CORS policies control which origins can access the Data API from a browser.
CORS applies to HTTP only
CORS is a browser-enforced security mechanism that applies to the HTTP/Data API. Native Postgres
TCP connections (port 5432/6432) are not subject to CORS. If you connect via psql, Prisma, or
any database driver, CORS does not apply.
Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Browser / Client │
│ │
│ fetch("https://data.axiom.cloud/v1/query", { │
│ headers: { Authorization: "Bearer dtk_xxx" } │
│ }) │
└──────────────────────────┬───────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ AxiomDB Data API Gateway │
│ │
│ 1. CORS preflight (OPTIONS) │
│ └─ Check Origin against allowed origins │
│ └─ Return Access-Control-Allow-* headers │
│ │
│ 2. Token verification │
│ └─ Validate PASETO token │
│ └─ Resolve scopes and rate limits │
│ │
│ 3. Query execution │
│ └─ Execute SQL against Postgres via PgBouncer │
│ └─ Apply row limits and timeouts │
│ │
│ 4. Response │
│ └─ Return JSON response with CORS headers │
│ └─ Log to audit trail │
└──────────────────────────────────────────────────────────────────┘CORS Configuration
Allowed Origins
You configure CORS at the project level. Only explicitly listed origins are allowed. Wildcards are supported but discouraged for production.
{
"cors": {
"allowed_origins": [
"https://app.example.com",
"https://staging.example.com",
"http://localhost:3000"
],
"allowed_methods": ["GET", "POST", "OPTIONS"],
"allowed_headers": ["Authorization", "Content-Type", "X-Request-Id"],
"exposed_headers": ["X-Request-Id", X-Rate-Limit-Remaining"],
"max_age": 86400,
"allow_credentials": true
}
}Setting CORS via CLI
axiom data-api cors set \
--project my-project \
--origins "https://app.example.com,https://staging.example.com,http://localhost:3000" \
--methods "GET,POST,OPTIONS" \
--max-age 86400Setting CORS via API
curl -X PUT "https://api.axiom.cloud/v1/projects/prj_abc123/data-api/cors" \
-H "Authorization: Bearer ptk_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"allowed_origins": [
"https://app.example.com",
"https://staging.example.com"
],
"allowed_methods": ["GET", "POST", "OPTIONS"],
"allowed_headers": ["Authorization", "Content-Type"],
"max_age": 86400,
"allow_credentials": true
}'CORS Headers
Preflight Response
When a browser sends an OPTIONS request, AxiomDB returns:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-Id
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
Vary: OriginActual Response
For GET and POST requests:
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: X-Request-Id, X-Rate-Limit-Remaining
Vary: Origin
Content-Type: application/json
{...}Vary: Origin
AxiomDB always includes Vary: Origin to prevent caching issues when multiple origins are
configured. This ensures CDN caches serve the correct CORS headers per origin.
Data API Tokens
Data API tokens are separate from management API tokens. They are scoped, revocable, named, rate limited, and audited.
Token schema
{
"token_id": "dtk_7xK2mP9nQ3w8",
"name": "Frontend Read Token",
"scopes": ["query:read"],
"branches": ["br_main", "br_staging"],
"rate_limit": {
"requests_per_minute": 100,
"rows_per_query": 10000
},
"expires_at": "2026-06-01T00:00:00Z",
"created_at": "2026-01-15T10:30:00Z",
"created_by": "usr_8xK2mP",
"last_used_at": "2026-01-15T14:22:00Z",
"status": "active"
}Token scopes
| Scope | Description |
|---|---|
query:read | Execute SELECT queries |
query:write | Execute INSERT, UPDATE, DELETE queries |
query:admin | Execute DDL and admin queries |
data:export | Export data in bulk |
Creating a token
axiom data-api token create \
--project my-project \
--name "Frontend Read Token" \
--scopes "query:read" \
--branches "main,staging" \
--rate-limit 100 \
--rows-limit 10000 \
--expires "90d"Output:
Token created successfully.
Token ID: dtk_7xK2mP9nQ3w8
Name: Frontend Read Token
Scopes: query:read
Branches: main, staging
Rate: 100 req/min, 10000 rows/query
Expires: 2026-04-15T10:30:00Z
Token: dtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
⚠ This token is shown once. Store it securely.Token display policy
Tokens are displayed exactly once at creation. The token value cannot be retrieved later. If you lose a token, create a new one and revoke the old token.
Listing tokens
axiom data-api token list --project my-projectTOKEN ID NAME SCOPES BRANCHES STATUS
dtk_7xK2mP9nQ3w8 Frontend Read Token query:read main,staging active
dtk_4nR8wP2xK9mL Backend Write Token query:write main active
dtk_aB3xK9mL7pQ4 CI Token query:admin * revokedRevoking a token
axiom data-api token revoke \
--project my-project \
--token dtk_7xK2mP9nQ3w8Revoked tokens are immediately invalid. All subsequent requests using the token will receive a
401 Unauthorized response.
Query Execution
Read query
curl -X POST "https://data.axiom.cloud/v1/query" \
-H "Authorization: Bearer dtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "X-Branch: main" \
-d '{
"query": "SELECT id, name, email FROM users WHERE active = $1 LIMIT $2",
"params": [true, 100]
}'Response:
{
"rows": [
{"id": 1, "name": "Alice", "email": "alice@example.com"},
{"id": 2, "name": "Bob", "email": "bob@example.com"}
],
"columns": [
{"name": "id", "type": "integer"},
{"name": "name", "type": "text"},
{"name": "email", "type": "text"}
],
"row_count": 2,
"duration_ms": 12,
"request_id": "req_4nR8wP2x"
}Write query
curl -X POST "https://data.axiom.cloud/v1/query" \
-H "Authorization: Bearer dtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "X-Branch: main" \
-d '{
"query": "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
"params": ["Charlie", "charlie@example.com"]
}'Response:
{
"rows": [{"id": 3}],
"columns": [{"name": "id", "type": "integer"}],
"row_count": 1,
"duration_ms": 8,
"request_id": "req_5nP3xK7m"
}Rate Limiting
Each token has configurable rate limits enforced at the gateway level.
| Limit | Default | Configurable |
|---|---|---|
| Requests per minute | 60 | Yes |
| Rows per query | 10,000 | Yes |
| Query timeout | 30s | Yes |
| Max payload size | 1MB | No |
Rate limit headers
Every response includes rate limit information:
X-Rate-Limit-Limit: 100
X-Rate-Limit-Remaining: 87
X-Rate-Limit-Reset: 1705312200Rate limit exceeded
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-Rate-Limit-Limit: 100
X-Rate-Limit-Remaining: 0
X-Rate-Limit-Reset: 1705312200
{
"error": "rate_limit_exceeded",
"message": "Token dtk_7xK2mP9nQ3w8 has exceeded the rate limit of 100 requests per minute",
"retry_after": 45
}Security Model
Input sanitization
The Data API uses parameterized queries exclusively. Raw string interpolation is never performed.
✅ {"query": "SELECT * FROM users WHERE id = $1", "params": [42]}
❌ {"query": "SELECT * FROM users WHERE id = 42"}Parameterized queries only
Queries must use $1, $2, etc. for parameters. Literal values embedded in the query string
are rejected with a 400 Bad Request response.
Row limits
Every query is subject to a maximum row limit. The default is 10,000 rows per query. This can be adjusted per token up to a project-level maximum.
{
"error": "row_limit_exceeded",
"message": "Query returned 15,234 rows, exceeding the limit of 10,000",
"row_count": 15234,
"row_limit": 10000
}Query timeout
Queries that exceed the timeout (default: 30 seconds) are terminated:
{
"error": "query_timeout",
"message": "Query exceeded the timeout of 30 seconds",
"duration_ms": 30001
}Branch isolation
Tokens can be scoped to specific branches. A token scoped to main cannot query staging:
{
"error": "forbidden",
"message": "Token dtk_7xK2mP9nQ3w8 does not have access to branch 'staging'",
"allowed_branches": ["main"]
}Masked Token Display
After creation, tokens are always displayed in masked form:
dtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxThis applies to:
- Dashboard token list
- API responses for token metadata
- Audit logs
- CLI output (except at creation time)
Audit Events
All Data API token operations are logged:
| Event | Description |
|---|---|
data_api.token.created | Token was created |
data_api.token.revoked | Token was revoked |
data_api.token.used | Token was used for a query (sampled) |
data_api.query.executed | Query was executed (includes masked token ID) |
Example audit event:
{
"event": "data_api.token.created",
"timestamp": "2026-01-15T10:30:00Z",
"actor": {
"id": "usr_8xK2mP",
"email": "alice@example.com"
},
"details": {
"token_id": "dtk_7xK2mP9nQ3w8",
"name": "Frontend Read Token",
"scopes": ["query:read"],
"branches": ["main", "staging"],
"expires_at": "2026-04-15T10:30:00Z"
}
}CORS Troubleshooting
Preflight blocked
Access to fetch at 'https://data.axiom.cloud/v1/query' from origin 'https://evil.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.Cause: The origin https://evil.com is not in the allowed origins list.
Fix: Add the origin to the CORS configuration:
axiom data-api cors set \
--project my-project \
--add-origin "https://new-app.example.com"Missing Authorization header
Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.Cause: The Authorization header is not in allowed_headers.
Fix: Update CORS configuration to include Authorization:
{
"allowed_headers": ["Authorization", "Content-Type"]
}Credentials not supported
The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*'
when the request's credentials mode is 'include'.Cause: allowed_origins is set to * but allow_credentials is true.
Fix: Use explicit origins instead of wildcards:
{
"allowed_origins": ["https://app.example.com"],
"allow_credentials": true
}Token not sent in CORS request
Cause: Browser does not include credentials for cross-origin requests by default.
Fix: Use credentials: 'include' in fetch:
const response = await fetch('https://data.axiom.cloud/v1/query', {
method: 'POST',
credentials: 'include',
headers: {
'Authorization': 'Bearer dtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: 'SELECT * FROM users LIMIT 10',
}),
});SDK Examples
JavaScript / TypeScript
import { AxiomDB } from '@axiomdb/client';
const client = new AxiomDB({
token: process.env.AXIOM_DATA_API_TOKEN,
branch: 'main',
});
const result = await client.query(
'SELECT id, name FROM users WHERE active = $1 LIMIT $2',
[true, 100]
);
console.log(result.rows);Python
import axiomdb
client = axiomdb.Client(
token=os.environ["AXIOM_DATA_API_TOKEN"],
branch="main",
)
result = client.query(
"SELECT id, name FROM users WHERE active = $1 LIMIT $2",
[True, 100]
)
for row in result.rows:
print(row)cURL
curl -X POST "https://data.axiom.cloud/v1/query" \
-H "Authorization: Bearer dtk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "X-Branch: main" \
-d '{
"query": "SELECT id, name FROM users WHERE active = $1 LIMIT $2",
"params": [true, 100]
}'Best Practices
- Use explicit origins. Never use
*in production CORS configurations. - Scope tokens narrowly. Give tokens access only to the branches they need.
- Set expiry. All tokens should have an expiration date.
- Use read-only scopes. Only grant
query:writewhen the client needs to mutate data. - Monitor rate limits. Set up alerts for tokens approaching their rate limits.
- Rotate tokens. Rotate Data API tokens alongside credential rotation.
- Use parameterized queries. Never interpolate user input into query strings.
- Set appropriate row limits. Prevent clients from accidentally pulling entire tables.