DNS Operations Guide
Documentation for DNS Operations Guide
Date: 2026-03-30 Status: Authoritative Audience: Platform engineers, service developers, and operators working with NNO DNS
This guide covers practical DNS tasks: registering a new NNO core service, understanding how platform DNS is provisioned automatically, adding a client custom domain, configuring CORS origins, and troubleshooting common failures.
For naming conventions and pattern definitions, see the authoritative reference: DNS Naming Convention.
1. Overview
NNO uses two DNS mechanisms depending on context:
- Workers Custom Domains — for NNO Core services (Pattern A). Declared in
wrangler.toml, applied automatically on deploy. - CF4SaaS Custom Hostnames — for Client Platform resources (Pattern B) and client custom domains. Provisioned programmatically by the Provisioning service.
The distinction matters: Pattern A requires no API calls and no Registry entries. Pattern B requires the Provisioning service and is tracked in the Registry.
2. Adding DNS for a New NNO Core Service
Use this when you are creating a new NNO operator service (e.g. notifications.svc.nno.app).
Step 1 — Choose a hostname
Follow Pattern A: <name>.svc.nno.app for production, <name>.svc.stg.nno.app for staging. See DNS Naming Convention §2 for the full hostname table and naming rules.
Step 2 — Add [[routes]] to wrangler.toml
Modelled on services/billing/wrangler.toml:
# PRODUCTION (default)
[[routes]]
pattern = "notifications.svc.nno.app"
custom_domain = true
# STG ENVIRONMENT
[[env.stg.routes]]
pattern = "notifications.svc.stg.nno.app"
custom_domain = trueThat is the entire DNS configuration. No DNS records, no CF API calls, no Registry entries — Cloudflare manages everything when custom_domain = true is set.
Step 3 — Deploy
# Production
cd services/notifications
wrangler deploy
# Staging
wrangler deploy --env stgCloudflare creates the DNS A/AAAA records and activates the custom domain automatically during deploy.
Step 4 — Verify
curl -I https://notifications.svc.nno.app
# Expect: HTTP/2 200 (or your health check status)
curl -I https://notifications.svc.stg.nno.appIf you receive a 522 or 523, the DNS record may not have propagated yet — wait 30–60 seconds and retry. See Troubleshooting for more.
Step 5 — Update the hostname table
Add the new service to the NNO Core Service Hostnames table in dns-naming.md §2.
3. How Platform DNS Gets Provisioned
Client platform DNS is fully automatic. This section explains what happens under the hood so you can diagnose problems when it does not.
Trigger
Platform creation enqueues a BOOTSTRAP_PLATFORM provisioning job. This job, among other things, calls registerDns() from services/provisioning/src/dns/register.ts for each resource in the default stack (starting with the auth Worker).
What registerDns() does
buildHostname({ name, type, stackId, platformId }) → prod hostname
buildHostname({ name, type, stackId, platformId, staging: true }) → stg hostname
CF4SaaS create prod hostname (idempotent — skips if already exists)
CF4SaaS create stg hostname (idempotent — skips if already exists)
Registry.createDnsRecord(...) → dns_records row (prod)
Registry.createDnsRecord(...) → dns_records row (stg)Both environments are always registered together. The CF4SaaS call is idempotent: if the hostname already exists (e.g. after a retry), registerDns() looks it up and reuses its ID rather than creating a duplicate.
Dual-environment example
For platform a1b2c3d4e5, auth Worker registration produces:
| Environment | Hostname |
|---|---|
| Production | auth.svc.default.a1b2c3d4e5.nno.app |
| Staging | auth.svc.stg.default.a1b2c3d4e5.nno.app |
Both rows appear in the Registry dns_records table with status = "pending" until CF4SaaS validates the SSL certificate.
SSL provisioning
CF4SaaS issues a DV certificate via HTTP validation automatically after the custom hostname is created. The ssl-poller cron (services/provisioning/src/cron/ssl-poller.ts) polls every 15 minutes:
- Fetches all
custom_domainsrecords withstatus = "pending"from the Registry - Queries the CF4SaaS API for each
cfHostnameId - If
ssl.status === "active"→ patches record tostatus = "active",sslStatus = "active" - If
verification_errorspresent → patches record tostatus = "failed",sslStatus = "failed"
Under normal conditions a new hostname is active within 15–30 minutes of provisioning.
4. Adding a Custom Domain
Clients can map their own domain (e.g. app.acmecorp.com) to any platform hostname. This is handled by the ADD_CUSTOM_DOMAIN provisioning job (services/provisioning/src/executors/add-custom-domain.ts).
Job inputs
The job uses two fields:
| Job field | Value |
|---|---|
featureId | The custom hostname to register (e.g. app.acmecorp.com) |
sharedResources | JSON \{ "targetDnsRecordId": "<dns_record id>" \} |
targetDnsRecordId is the ID of an existing dns_records row — the NNO hostname the custom domain should resolve to.
Three-step pipeline
Step 1 — resolve_target: Fetches the target dns_records row from the Registry to get the NNO hostname.
Step 2 — create_custom_hostname: Creates a CF4SaaS custom hostname for app.acmecorp.com pointing to the NNO zone. Returns a cfHostnameId.
Step 3 — register_custom_domain: Creates a custom_domains row in the Registry linking the custom hostname to the target DNS record.
Step 3 is non-fatal: if the Registry call fails, the CF4SaaS hostname is already created and the domain can be re-registered without re-running the CF step.
Client DNS action required
After the job completes, the client must add a CNAME in their DNS provider:
app.acmecorp.com CNAME auth.svc.default.<pid>.nno.appSSL verification begins as soon as the CNAME resolves. The ssl-poller picks up the pending custom_domains record and updates status once CF4SaaS confirms the certificate is active.
Tracking status
Query the Registry custom_domains table for the platform. The sslStatus field progresses from pending → active (or failed on error). See DNS Naming Convention §8 for the full column reference.
5. CORS Configuration
CORS allowed origins are configured as CORS_ORIGINS — a comma-separated string set either as a [vars] entry in wrangler.toml or as a wrangler secret.
Production pattern — single origin
[vars]
CORS_ORIGINS = "https://console.app.nno.app"Production only allows the canonical console origin. No preview URLs.
Staging pattern — multiple origins including Pages previews
[env.stg.vars]
CORS_ORIGINS = "https://console.app.stg.nno.app,https://nno-k3m9p2xw7q-console.pages.dev,https://develop.nno-k3m9p2xw7q-console.pages.dev,https://nno-k3m9p2xw7q-backoffice-stg.pages.dev"Staging includes the stable staging origin plus Cloudflare Pages branch preview URLs so that PR deploys can call backend services without CORS errors.
When to update CORS_ORIGINS
| Scenario | Action |
|---|---|
| New NNO frontend app added | Add its production origin to every backend service that the frontend calls directly |
| New Pages branch preview needed for testing | Add the *.pages.dev URL to staging CORS_ORIGINS of relevant services |
| Client platform custom domain | Client platforms manage their own auth Worker CORS — this does not affect NNO core services |
Update [vars] in wrangler.toml for values that can be public. Use wrangler secret put CORS_ORIGINS --env <stg|prod> when the origin list should not be checked into source control (uncommon for CORS origins, but appropriate if the list contains internal or sensitive URLs).
6. Troubleshooting
Custom domain SSL pending for more than 30 minutes
The ssl-poller runs every 15 minutes. If a domain stays in pending longer than 30 minutes:
- Check provisioning logs for the
BOOTSTRAP_PLATFORMorADD_CUSTOM_DOMAINjob — confirm step 2 (create_custom_hostname) completed successfully and acfHostnameIdwas recorded. - Verify the CNAME is in place:
dig CNAME app.acmecorp.comshould return the NNO hostname. - Query the CF4SaaS API directly for the
cfHostnameIdand inspectverification_errors. - If errors are present, the Registry record will be patched to
failedon the next poll cycle. Fix the CNAME and reset the record status topendingto trigger re-verification.
Service returns 522 or 523
These are Cloudflare errors indicating the Worker did not respond (522) or the origin was unreachable (523).
- Confirm the
[[routes]]entry exists inwrangler.tomlwithcustom_domain = true. - Confirm the deploy succeeded:
wrangler deployments list. - Cloudflare Custom Domain DNS records are created asynchronously — wait 30–60 seconds after a first deploy.
- If the service was recently renamed, the old DNS record may still exist and conflict. Remove it from the Cloudflare DNS dashboard.
CORS error in browser
Access to fetch at 'https://iam.svc.nno.app/...' from origin 'https://myapp.app.nno.app' has been blocked by CORS policy- Identify which backend service is returning the CORS error (check the network tab for the failing request URL).
- Open that service's
wrangler.tomland verify the frontend origin is inCORS_ORIGINSfor the correct environment. - If
CORS_ORIGINSis set as a secret (not inwrangler.toml), runwrangler secret put CORS_ORIGINS --env <env>with the updated value. - Redeploy:
wrangler deploy [--env stg].
Auth cookie not shared across apps on the same platform
The auth cookie is set with domain .<pid>.nno.app (note the leading dot). If a frontend is served from a hostname that does not share this ancestor domain, the cookie will not be sent.
- Verify the frontend hostname matches Pattern B:
<name>.app.<stackId>.<pid>.nno.app - Verify the auth Worker is on the default stack:
auth.svc.default.<pid>.nno.app - Confirm the cookie
Domainattribute is.<pid>.nno.app— see DNS Naming Convention §5 for the full cookie domain configuration table. - In development, both services must run on
localhostfor the cookie to be shared (thelocalhostcookie domain covers all localhost ports).