Domains and secrets
Attach your own domain with automatic TLS, and configure an app's credentials as per-environment worker secrets — including the reserved names the platform manages for you.
Two pieces of per-environment configuration: the hostname the world reaches your app on, and the credentials your app uses.
Custom domains
Every app gets a platform URL immediately. Attaching your own domain is two steps.
1. Register the domain with the app:
sf add_custom_domain --customDomain www.example.com
The response includes the CNAME record to create — a target hostname to point at — plus the current TLS status.
2. Create that CNAME at your DNS provider:
CNAME www.example.com → <target printed by the command>
TLS is then issued automatically, typically 5–15 minutes after DNS propagates. Check progress with:
sf list_custom_domains
Each entry shows the domain, its environment, an overall status, and separate ssl and hostname states. All three go active when the domain is fully live:
{
"customDomain": "www.spikefrost.com",
"environment": "production",
"status": "active",
"ssl": { "status": "active", "isActive": true }
}
Notes worth having in advance:
- Domains are per environment.
www.example.comon production andstaging.example.comon staging is the normal arrangement. - A CNAME can't sit on a bare apex domain under standard DNS rules. Either use
wwwand redirect the apex at your DNS provider, or use a provider that supports apex flattening / ALIAS records. - Everything keeps working on the platform URL — adding a domain doesn't take the original away.
- The domain must be one you control. Point DNS you own; the CNAME step is what actually routes traffic.
Worker secrets
Anything your app needs and must not commit — a payment provider key, a third-party API token, a signing secret.
sf set_worker_secret --secretName STRIPE_SECRET_KEY --secretValue sk_live_...
sf list_worker_secrets
sf delete_worker_secret --secretName STRIPE_SECRET_KEY
Read them in Worker code like any binding, after declaring the type in Env:
export interface Env {
// ...
STRIPE_SECRET_KEY?: string;
}
app.post('/api/checkout', async (c) => {
const key = c.env.STRIPE_SECRET_KEY;
if (!key) return c.json({ error: 'not configured' }, 500);
// ...
});
The rules:
- Per environment. Production and staging hold separate values — which is the point: staging uses test keys.
- The app must have been deployed at least once before it can hold secrets; there's no environment to attach them to otherwise.
- Values are never readable back. Listing returns names only. Changing a secret means setting it again.
- They survive deploys. Each deploy reattaches them, and the deployment event stream includes a
SECRETSstep. - Never put a secret in source, in
wrangler.toml, or in client-side code. Server-side only, always.
Reserved names
Some names are managed by the platform and rejected if you try to set them:
| Name | What it is |
|---|---|
SPIKE_TENANT_API_KEY |
Auth for platform capability calls |
SPIKE_ROUTINE_SIGNING_SECRET |
Verifies scheduled and queue callbacks into your app |
SPIKE_PROJECT_ID |
This app's id |
PUBLIC_URL |
The app's public URL |
These are injected at deploy time and always correct — including after a clone, which is exactly why you should read c.env.SPIKE_PROJECT_ID rather than hardcoding an app id anywhere.
Treat the tenant key as highly sensitive. It can reach every integration your team has configured. It belongs on the server, never in a response, never in a log line, never in the browser.
Choosing where configuration lives
| Kind of value | Where |
|---|---|
| Credentials, API keys, signing secrets | Worker secrets |
| Values that differ per environment but aren't secret | Worker secrets (simplest) or a config table |
| Settings an admin should be able to change at runtime | A database table, edited through your own admin UI |
| Feature flags you flip without a deploy | KV (c.env.KV) |
| Non-secret constants | Source code |
The distinction that matters: a secret needs a deploy to take effect for new code paths, while KV and database values change live. Flags you'll flip during an incident belong in KV.
Next
- Build and deploy — the
SECRETSstep in a deployment's events. - Environments — why per-environment configuration is the default.
- Files and assets — calling third-party services with platform-held credentials instead of your own keys.
Frequently asked questions
What DNS record do I need for a custom domain?
A CNAME pointing your hostname at the target the add command prints. TLS is issued automatically, usually within 5–15 minutes of DNS propagating.
Can two environments have different domains?
Yes. Domains are attached per environment, so staging.example.com can point at your staging environment while www.example.com serves production.
Are secrets visible after I set them?
No. Listing secrets returns names only, never values. To change one, set it again; to remove it, delete it by name.
Do secrets survive a deploy?
Yes — they're part of the environment's configuration, and each deploy reattaches them. Deployment events include a SECRETS step so you can confirm.
Why was my secret name rejected?
Some names are reserved and provisioned automatically by the platform — SPIKE_TENANT_API_KEY, SPIKE_ROUTINE_SIGNING_SECRET, SPIKE_PROJECT_ID, and PUBLIC_URL. Use your own names for your own credentials.