Security

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 86400

Setting 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: Origin

Actual 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

ScopeDescription
query:readExecute SELECT queries
query:writeExecute INSERT, UPDATE, DELETE queries
query:adminExecute DDL and admin queries
data:exportExport 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-project
TOKEN 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  *               revoked

Revoking a token

axiom data-api token revoke \
  --project my-project \
  --token dtk_7xK2mP9nQ3w8

Revoked 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.

LimitDefaultConfigurable
Requests per minute60Yes
Rows per query10,000Yes
Query timeout30sYes
Max payload size1MBNo

Rate limit headers

Every response includes rate limit information:

X-Rate-Limit-Limit: 100
X-Rate-Limit-Remaining: 87
X-Rate-Limit-Reset: 1705312200

Rate 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_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

This 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:

EventDescription
data_api.token.createdToken was created
data_api.token.revokedToken was revoked
data_api.token.usedToken was used for a query (sampled)
data_api.query.executedQuery 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

  1. Use explicit origins. Never use * in production CORS configurations.
  2. Scope tokens narrowly. Give tokens access only to the branches they need.
  3. Set expiry. All tokens should have an expiration date.
  4. Use read-only scopes. Only grant query:write when the client needs to mutate data.
  5. Monitor rate limits. Set up alerts for tokens approaching their rate limits.
  6. Rotate tokens. Rotate Data API tokens alongside credential rotation.
  7. Use parameterized queries. Never interpolate user input into query strings.
  8. Set appropriate row limits. Prevent clients from accidentally pulling entire tables.

On this page