Skip to main content

Service Keys

Server-side API keys that bypass access rules. Service Keys are used by the Admin SDK to authenticate backend operations — admin scripts, CI/CD pipelines, and microservice-to-microservice calls.

What is a Service Key?

A Service Key is any string you choose (e.g. my-super-secret-key-123). There is no special format or generation step — just pick a strong, random string and store it as a secret.

Quick Start

1. Define Keys in Config

edgebase.config.ts
import { defineConfig } from '@edgebase/shared';

export default defineConfig({
serviceKeys: {
keys: [
{
kid: 'backend',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_BACKEND',
},
],
},
});

2. Set the Secret

The secret value is any string you choose. Pick something long and random:

Add the secret to your environment file. Pick something long and random:

# .env.development (local) or .env.release (production)
SERVICE_KEY_BACKEND=my-super-secret-key-abc123xyz

Production secrets in .env.release are uploaded automatically when you run npx edgebase deploy.

3. Use with the Admin SDK

Service Keys authenticate the Admin SDK. Pass the same string you stored as a secret:

import { createAdminClient } from '@edgebase/admin';

const admin = createAdminClient('https://your-worker.workers.dev', {
serviceKey: process.env.SERVICE_KEY_BACKEND, // The string you chose
});

// All operations bypass access rules
const posts = await admin.db('shared').table('posts').getList();

See Admin SDK for full setup instructions for all Admin SDKs.

For raw HTTP requests, use X-EdgeBase-Service-Key as the canonical header. Some routes also accept Authorization: Bearer ..., but the dedicated header is the stable public contract.

Captcha Bypass

Requests authenticated with a Service Key automatically bypass captcha verification. This allows all Admin SDKs and backend automation to call auth-related endpoints without a Turnstile token. See Captcha for details.

Key Tiers

TierDescriptionUse Case
rootFull access — bypasses all rules and scopesAdmin scripts, migrations
scopedOnly allowed for matching scopesMicroservice-to-microservice

Scoped Key Example

{
kid: 'analytics',
tier: 'scoped',
scopes: ['db:table:events:write'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_ANALYTICS',
}

This key can only write to the events table. Any other operation returns 401.

Scope Format

{domain}:{resource-type}:{resource-name}:{action}
ScopeMeaning
*Full access (root only)
db:table:posts:readRead posts
db:table:*:readRead any table
storage:bucket:photos:writeWrite to photos bucket
storage:bucket:*:*Full storage access
push:notification:*:sendSend push notifications
push:token:*:readRead device tokens
push:log:*:readRead push logs
sql:table:posts:execRun raw SQL on posts
realtime:channel:*:broadcastBroadcast to any channel
kv:namespace:cache:readRead from cache namespace
kv:namespace:cache:writeWrite to cache namespace
d1:database:analytics:execRun SQL on analytics D1
vectorize:index:embeddings:queryQuery embeddings index

Secret Management

Secret is stored as a Cloudflare Workers Secret — never in code.

{
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_BACKEND', // env var name in Workers
}

inline (Local Dev Only)

Secret is written directly in config. Never use in production.

{
secretSource: 'inline',
inlineSecret: 'my-dev-secret',
}
warning

npx edgebase deploy will warn if any key uses secretSource: 'inline'.

Key Format

When configuring secretSource: 'dashboard', the secret value you store can be any string. Optionally, you can use the structured jb_{kid}_{secret} format to enable faster lookup:

# .env.development or .env.release
# Structured format (recommended for scoped keys): jb_{kid}_{your-secret-value}
SERVICE_KEY_ANALYTICS=jb_analytics_abc123xyz456secretvalue

# Or plain string (works for root-tier keys):
SERVICE_KEY_BACKEND=mysecretkey-abc123

The kid in jb_{kid}_{...} must match the kid field in your config:

{
kid: 'analytics', // ← must match the jb_analytics_... prefix
secretRef: 'SERVICE_KEY_ANALYTICS',
}

kid values must use letters, numbers, and hyphens only. Underscore is reserved as the delimiter in jb_{kid}_{secret}.

For root-tier keys, any plain string works — the jb_ prefix is optional.

If you use auth hooks, storage hooks, plugin migrations, or function admin helpers, keep one dedicated root-tier key for internal self-calls. The recommended setup is secretRef: 'SERVICE_KEY' with no tenant or ipCidr constraint:

{
kid: 'root',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY',
}

This same canonical root key is what npx edgebase admin reset-password uses for admin recovery, so it should exist in every environment where you operate the Admin Dashboard.

Constraints

Constraints add conditions that must pass in addition to scope matching.

{
kid: 'prod-backend',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_PROD',
constraints: {
expiresAt: '2025-12-31T23:59:59Z',
env: ['prod'],
ipCidr: ['10.0.0.0/8', '172.16.0.0/12'],
tenant: 'workspace-123',
},
}

expiresAt

ISO 8601 timestamp. Key is rejected after this time.

constraints: { expiresAt: '2025-06-30T00:00:00Z' }

env

Restrict key to specific server environments. Compared against the ENVIRONMENT env var.

constraints: { env: ['prod'] }          // Only works in production
constraints: { env: ['dev', 'staging'] } // Dev + staging only

Set your environment name in wrangler.toml:

wrangler.toml
[vars]
ENVIRONMENT = "prod"

ipCidr

Restrict to specific IP ranges. Supports IPv4 and IPv6 CIDR notation.

constraints: {
ipCidr: [
'10.0.0.0/8', // Internal network
'172.16.0.0/12', // Docker default
'2001:db8::/32', // IPv6 range
],
}

The client IP is extracted from cf-connecting-ip (Cloudflare) or x-forwarded-for headers.

tenant

Restrict to a specific tenant. Matched against the DB instance ID (namespace:id) in the request path.

constraints: { tenant: 'workspace-123' }

When using user-namespaced DB blocks, the namespace and instance ID are passed via the URL:

// client.db('workspace', 'workspace-123').table('docs') — namespace+id in URL 
// This key only works when the instance ID matches 'workspace-123'

Behavior When Context is Missing

All constraints except expiresAt are fail-closed:

ConstraintContext MissingResult
envENVIRONMENT not set❌ Denied
ipCidrNo IP header (e.g. Docker)❌ Denied
tenantNo instance ID in path❌ Denied
expiresAtAlways enforced

This prevents constrained keys from silently becoming broader when the server cannot prove the required context. In local development, use unconstrained keys or provide matching ENVIRONMENT, client IP forwarding, and DB instance IDs.

Full Config Example

edgebase.config.ts
export default defineConfig({
serviceKeys: {
keys: [
// Root key for admin operations
{
kid: 'admin',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_ADMIN',
constraints: {
env: ['prod'],
ipCidr: ['10.0.0.0/8'],
},
},
// Scoped key for analytics microservice
{
kid: 'analytics',
tier: 'scoped',
scopes: ['db:table:events:write'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_ANALYTICS',
constraints: {
expiresAt: '2026-01-01T00:00:00Z',
},
},
// Local dev key (never deploy to production)
{
kid: 'local',
tier: 'root',
scopes: ['*'],
secretSource: 'inline',
inlineSecret: 'dev-secret-123',
constraints: {
env: ['dev'],
},
},
],
},
});

Key Rotation

To rotate a key without downtime:

  1. Add the new key entry with a different kid
  2. Deploy the config
  3. Update your backend to use the new key
  4. Disable the old key: enabled: false
  5. Deploy again
{
kid: 'backend-v1',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_V1',
enabled: false, // Disabled — no longer accepted
},
{
kid: 'backend-v2',
tier: 'root',
scopes: ['*'],
secretSource: 'dashboard',
secretRef: 'SERVICE_KEY_V2',
},
Current Contract

Define every Service Key in config.serviceKeys. The default root key usually references the SERVICE_KEY Worker secret via secretRef, and npx edgebase keys rotate replaces that secret immediately.