PlaycademyPlaycademy

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:

.env
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 NamePurposePriority
.envBase configurationLowest
.env.developmentDevelopment-specific overridesMiddle
.env.localLocal overridesHighest

Make sure .env files are listed in your .gitignore:

.gitignore
.env
.env.local
.env.development

Accessing Secrets

Access secrets in your custom routes via c.env.secrets:

server/api/ai-prompt.ts
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())
}
server/api/sign-message.ts
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 })
}
server/api/webhooks/stripe.ts
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:

server/api/example.ts
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:

Command
$ playcademy secrets push$ playcademy secrets list

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

Command
$ playcademy db seed --remote --force$ playcademy db seed --remote -f

The --force flag skips the secrets sync prompt but still passes your local .env secrets to the seed worker.


What's Next?

On this page