Secrets
Manage API keys and sensitive configuration for your backend
Overview
Secrets are environment variables that store sensitive data for use in your backend.
API keys, signing secrets, and credentials that should never be committed to version control.
Secrets are perfect for:
- Third-party API keys
- Encryption keys and signing secrets
- Any sensitive configuration
Never Commit Secrets
Secrets should never be committed to version control or exposed to the client.
Store them in your .env file which should be gitignored.
The .env File
Store secrets in an .env file at your project root:
SECRET_KEY=mysecret123
API_KEY=sk_live_abc123xyz
WEBHOOK_SECRET=whsec_abc123
OPENAI_API_KEY=sk-proj-...Multiple .env files are supported with the following priority:
| File Name | Purpose | Priority |
|---|---|---|
.env | Base configuration | Lowest |
.env.development | Development-specific overrides | Middle |
.env.local | Local overrides | Highest |
Make sure .env files are listed in your .gitignore:
.env
.env.local
.env.developmentAccessing Secrets
Access secrets in your custom routes via c.env.secrets:
export async function POST(c: Context) {
const headers = new Headers()
const apiKey = c.env.secrets.OPENAI_API_KEY
if (!apiKey) {
return c.json({ error: 'API key not configured' }, 500)
}
headers.set('Authorization', `Bearer ${apiKey}`)
headers.set('Content-Type', 'application/json')
const data = {
model: 'gpt-4',
messages: [{ role: 'user', content: 'Hello!' }],
}
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers,
body: JSON.stringify(data),
})
return c.json(await response.json())
}export async function POST(c: Context) {
const { message } = await c.req.json()
const secretKey = c.env.secrets.SIGNING_KEY
const encoder = new TextEncoder()
const keyData = encoder.encode(secretKey)
const messageData = encoder.encode(message)
const key = await crypto.subtle.importKey(/** derive key from secret key */)
const signature = await crypto.subtle.sign('HMAC', key, messageData)
const signatureHex = Array.from(new Uint8Array(signature))
return c.json({ message, signature: signatureHex })
}export async function POST(c: Context) {
const signature = c.req.header('stripe-signature')
const webhookSecret = c.env.secrets.STRIPE_WEBHOOK_SECRET
const isValid = verifyStripeSignature(await c.req.text(), signature, webhookSecret)
return c.json({ received: true, isValid }, isValid ? 200 : 401)
}The CLI automatically generates local types, giving you autocomplete and compile-time safety:
export async function POST(c: Context) {
// ✅ Autocomplete suggests: OPENAI_API_KEY, STRIPE_SECRET, etc.
// ✅ TypeScript knows these exist and are strings
const apiKey = c.env.secrets.OPENAI_API_KEY
// ❌ TypeScript error: Property 'TYPO_KEY' doesn't exist
const oops = c.env.secrets.TYPO_KEY
}Deploying Secrets
When you run playcademy deploy, secrets from your .env file are automatically pushed.
If secrets are the only change, the CLI pushes them without redeploying your code.
Alternative Commands
If you need to push secrets without deploying, use the dedicated commands:
$ playcademy secrets push$ playcademy secrets listBoth commands accept --env production to target production.
Removing Secrets
Delete a secret from your .env file and deploy. The CLI will remove it from remote.
Database Seeding
When running playcademy db seed --remote, the CLI checks if your local secrets match remote.
If they're out of sync, you'll be prompted to push before seeding.
This ensures your seed script has access to the same secrets as your deployed backend via c.env.secrets.
Skipping the Secrets Check
If your seed script doesn't need secrets, you can skip the sync check:
$ playcademy db seed --remote --force$ playcademy db seed --remote -fThe --force flag skips the secrets sync prompt but still passes your local .env secrets to the seed worker.
