NNO Docs
Guides

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 = true

That 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 stg

Cloudflare 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.app

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

EnvironmentHostname
Productionauth.svc.default.a1b2c3d4e5.nno.app
Stagingauth.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:

  1. Fetches all custom_domains records with status = "pending" from the Registry
  2. Queries the CF4SaaS API for each cfHostnameId
  3. If ssl.status === "active" → patches record to status = "active", sslStatus = "active"
  4. If verification_errors present → patches record to status = "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 fieldValue
featureIdThe custom hostname to register (e.g. app.acmecorp.com)
sharedResourcesJSON \{ "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.app

SSL 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 pendingactive (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

ScenarioAction
New NNO frontend app addedAdd its production origin to every backend service that the frontend calls directly
New Pages branch preview needed for testingAdd the *.pages.dev URL to staging CORS_ORIGINS of relevant services
Client platform custom domainClient 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:

  1. Check provisioning logs for the BOOTSTRAP_PLATFORM or ADD_CUSTOM_DOMAIN job — confirm step 2 (create_custom_hostname) completed successfully and a cfHostnameId was recorded.
  2. Verify the CNAME is in place: dig CNAME app.acmecorp.com should return the NNO hostname.
  3. Query the CF4SaaS API directly for the cfHostnameId and inspect verification_errors.
  4. If errors are present, the Registry record will be patched to failed on the next poll cycle. Fix the CNAME and reset the record status to pending to 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 in wrangler.toml with custom_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
  1. Identify which backend service is returning the CORS error (check the network tab for the failing request URL).
  2. Open that service's wrangler.toml and verify the frontend origin is in CORS_ORIGINS for the correct environment.
  3. If CORS_ORIGINS is set as a secret (not in wrangler.toml), run wrangler secret put CORS_ORIGINS --env <env> with the updated value.
  4. Redeploy: wrangler deploy [--env stg].

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 Domain attribute is .<pid>.nno.app — see DNS Naming Convention §5 for the full cookie domain configuration table.
  • In development, both services must run on localhost for the cookie to be shared (the localhost cookie domain covers all localhost ports).

On this page