Patterns
Secret Rotation
Zero-downtime rotation with versioned naming (api_key_v1, api_key_v2):
interface Env {
PRIMARY_KEY: { get(): Promise<string> }
FALLBACK_KEY?: { get(): Promise<string> }
}
async function fetchWithAuth(url: string, key: string) {
return fetch(url, { headers: { Authorization: `Bearer ${key}` } })
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
let resp = await fetchWithAuth('https://api.example.com', await env.PRIMARY_KEY.get())
// Fallback during rotation
if (!resp.ok && env.FALLBACK_KEY) {
resp = await fetchWithAuth('https://api.example.com', await env.FALLBACK_KEY.get())
}
return resp
},
}
Workflow: Create api_key_v2 → add fallback binding → deploy → swap primary → deploy → remove v1
Encryption with KV
interface Env {
CACHE: KVNamespace
ENCRYPTION_KEY: { get(): Promise<string> }
}
async function encryptValue(value: string, key: string): Promise<string> {
const enc = new TextEncoder()
const keyMaterial = await crypto.subtle.importKey('raw', enc.encode(key), { name: 'AES-GCM' }, false, ['encrypt'])
const iv = crypto.getRandomValues(new Uint8Array(12))
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, keyMaterial, enc.encode(value))
const combined = new Uint8Array(iv.length + encrypted.byteLength)
combined.set(iv)
combined.set(new Uint8Array(encrypted), iv.length)
return btoa(String.fromCharCode(...combined))
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const key = await env.ENCRYPTION_KEY.get()
const encrypted = await encryptValue('sensitive-data', key)
await env.CACHE.put('user:123:data', encrypted)
return Response.json({ ok: true })
},
}
HMAC Signing
interface Env {
HMAC_SECRET: { get(): Promise<string> }
}
async function signRequest(data: string, secret: string): Promise<string> {
const enc = new TextEncoder()
const key = await crypto.subtle.importKey('raw', enc.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, [
'sign',
])
const sig = await crypto.subtle.sign('HMAC', key, enc.encode(data))
return btoa(String.fromCharCode(...new Uint8Array(sig)))
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const secret = await env.HMAC_SECRET.get()
const payload = await request.text()
const signature = await signRequest(payload, secret)
return Response.json({ signature })
},
}
Audit & Monitoring
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext) {
const startTime = Date.now()
try {
const apiKey = await env.API_KEY.get()
const resp = await fetch('https://api.example.com', {
headers: { Authorization: `Bearer ${apiKey}` },
})
ctx.waitUntil(
fetch('https://log.example.com/log', {
method: 'POST',
body: JSON.stringify({
event: 'secret_used',
secret_name: 'API_KEY',
timestamp: new Date().toISOString(),
duration_ms: Date.now() - startTime,
success: resp.ok,
}),
}),
)
return resp
} catch (error) {
ctx.waitUntil(
fetch('https://log.example.com/log', {
method: 'POST',
body: JSON.stringify({
event: 'secret_access_failed',
secret_name: 'API_KEY',
error: error instanceof Error ? error.message : 'Unknown',
}),
}),
)
return new Response('Error', { status: 500 })
}
},
}
Migration from Worker Secrets
Change env.SECRET (direct) to await env.SECRET.get() (async).
Steps:
- Create in Secrets Store:
wrangler secrets-store secret create <store-id> --name API_KEY --scopes workers --remote - Add binding to
wrangler.jsonc:{"binding": "API_KEY", "store_id": "abc123", "secret_name": "api_key"} - Update code:
const key = await env.API_KEY.get(); - Test staging, deploy
- Remove old:
wrangler secret delete API_KEY
Sharing Across Workers
Same secret, different binding names:
// worker-1: binding="SHARED_DB", secret_name="postgres_url"
// worker-2: binding="DB_CONN", secret_name="postgres_url"
JSON Secret Parsing
Store structured config as JSON secrets:
interface Env {
DB_CONFIG: { get(): Promise<string> }
}
interface DbConfig {
host: string
port: number
username: string
password: string
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
const configStr = await env.DB_CONFIG.get()
const config: DbConfig = JSON.parse(configStr)
// Use parsed config
const dbUrl = `postgres://${config.username}:${config.password}@${config.host}:${config.port}`
return Response.json({ connected: true })
} catch (error) {
if (error instanceof SyntaxError) {
return new Response('Invalid config JSON', { status: 500 })
}
throw error
}
},
}
Store JSON secret:
echo '{"host":"db.example.com","port":5432,"username":"app","password":"secret"}' | \
wrangler secrets-store secret create <store-id> \
--name DB_CONFIG --scopes workers --remote
Integration
Service Bindings
Auth Worker signs JWT with Secrets Store; API Worker verifies via service binding.
See: workers for service binding patterns.
See: api.md, gotchas.md