NNO DNS Naming Convention
Documentation for NNO DNS Naming Convention
Date: 2026-03-30
Status: Authoritative
Scope: All NNO apps and services under nno.app
1. Overview
All NNO apps and services are hosted under the nno.app domain, managed on Cloudflare. Two DNS patterns govern the entire namespace:
- Pattern A — NNO Core: short-form hostnames for Neutrino's own operator services
- Pattern B — Client Platform: structured hostnames for client platform apps and services
Type suffix convention:
.app= user-facing frontend (Cloudflare Pages).svc= backend API (Cloudflare Workers)
2. Pattern A — NNO Core
NNO operator services use a compact two-segment prefix before nno.app.
<name>.<type>.nno.app # production
<name>.<type>.stg.nno.app # stagingNNO Core Service Hostnames
| Service | Production | Staging |
|---|---|---|
| Console (operator UI) | console.app.nno.app | console.app.stg.nno.app |
| Gateway | gateway.svc.nno.app | gateway.svc.stg.nno.app |
| IAM | iam.svc.nno.app | iam.svc.stg.nno.app |
| Registry | registry.svc.nno.app | registry.svc.stg.nno.app |
| Billing | billing.svc.nno.app | billing.svc.stg.nno.app |
| Provisioning | provisioning.svc.nno.app | provisioning.svc.stg.nno.app |
| CLI Service | cli.svc.nno.app | cli.svc.stg.nno.app |
| Stack Registry | stack-registry.svc.nno.app | stack-registry.svc.stg.nno.app |
| Documentation | docs.app.nno.app | doc.app.stg.nno.app |
Public aliases (CNAME records):
api.nno.app→gateway.svc.nno.appapi.stg.nno.app→gateway.svc.stg.nno.appconsole.nno.app→console.app.nno.app(short alias for operator convenience)docs.nno.app→docs.app.nno.app(short alias for documentation site)
App Deployment Model
NNO apps are hosted on Cloudflare Pages. Each environment uses a separate Pages project to ensure full isolation (env vars are baked into the JS bundle at build time):
| Environment | Pages Project | Custom Domain | Build Mode |
|---|---|---|---|
| Production | nno-k3m9p2xw7q-console | console.app.nno.app, console.nno.app | build:prod (.env.prod) |
| Staging | nno-k3m9p2xw7q-console-stg | console.app.stg.nno.app | build:stg (.env.stg) |
Why separate projects? CF Pages custom domains always route to the production deployment of a project. A single project with preview branches cannot serve staging on a custom domain. Separate projects ensure staging serves the staging build with staging service URLs.
Phase 3 consideration: At scale, per-environment Pages projects double quota usage. A future staging environment routing Worker on
*.stg.nno.appcould proxy to branch preview URLs (develop.\{project\}.pages.dev), eliminating the need for separate-stgprojects. See Phase 3 implementation plans.
3. Pattern B — Client Platform
Client platform resources follow a four-segment prefix before nno.app.
<name>.<type>.<stack-id>.<platform-id>.nno.app # production
<name>.<type>.stg.<stack-id>.<platform-id>.nno.app # stagingSegment Definitions
| Segment | Description | Format |
|---|---|---|
<name> | App or service name (user-chosen) | Valid DNS label: [a-z][a-z0-9-]*[a-z0-9] — starts with a letter, ends with letter or digit, hyphens allowed in middle |
<type> | Deployment type | app (Pages — frontend/static) or svc (Worker — backend API) |
stg | Staging indicator | Literal stg; absent in production |
<stack-id> | Stack identifier | default (reserved keyword for the auto-created platform stack) or a 10-char nano-id [a-z0-9]. In the registry database the default stack uses a real nano-id with is_default = 1; the literal default is used only in DNS hostnames and CF resource names. |
<platform-id> | Platform identifier | 10-char nano-id [a-z0-9] |
Concrete Examples — Platform a1b2c3d4e5
Default stack (auth lives here, auto-created per tenant):
| Resource | Hostname |
|---|---|
| Auth Worker (prod) | auth.svc.default.a1b2c3d4e5.nno.app |
| Auth Worker (staging) | auth.svc.stg.default.a1b2c3d4e5.nno.app |
Stack x7y8z9w0q1 (e.g. "Marketing"):
| Resource | Hostname |
|---|---|
| Dashboard app (prod) | dashboard.app.x7y8z9w0q1.a1b2c3d4e5.nno.app |
| Dashboard API (prod) | dashboard.svc.x7y8z9w0q1.a1b2c3d4e5.nno.app |
| Dashboard app (staging) | dashboard.app.stg.x7y8z9w0q1.a1b2c3d4e5.nno.app |
| Dashboard API (staging) | dashboard.svc.stg.x7y8z9w0q1.a1b2c3d4e5.nno.app |
4. Hostname Parsing
The parseHostname utility uses a segment-count heuristic to determine which pattern applies.
Count the subdomain segments before nno.app:
| Segment count | Pattern | Environment |
|---|---|---|
| 2 | Pattern A — NNO Core | Production |
| 3 | Pattern A — NNO Core | Staging (contains stg) |
| 4 | Pattern B — Client Platform | Production |
| 5 | Pattern B — Client Platform | Staging (contains stg) |
Rule: 2–3 segments = Pattern A; 4–5 segments = Pattern B. Odd count (3 or 5) = staging.
5. Cookie Domain Configuration
Auth workers live under the default stack (auth.svc.default.<pid>.nno.app). App frontends may live under any stack. The common ancestor domain for all resources in a platform is .<pid>.nno.app, which is used as the cookie domain to enable seamless session sharing across stacks.
| Environment | Shell Origin | Auth Origin | Cookie Domain | SameSite | Secure |
|---|---|---|---|---|---|
| Development | localhost:5174 | localhost:8787 | localhost | Lax | false |
| Production | <name>.app.<stack>.<pid>.nno.app | auth.svc.default.<pid>.nno.app | .<pid>.nno.app | None | true |
| Staging | <name>.app.stg.<stack>.<pid>.nno.app | auth.svc.stg.default.<pid>.nno.app | .<pid>.nno.app | None | true |
Key point: Because all platform resources share the .<pid>.nno.app ancestor, the auth cookie set by auth.svc.default.<pid>.nno.app is accessible to all apps on the platform regardless of which stack they belong to.
6. SSL & Certificate Strategy
Cloudflare's Universal SSL covers only *.nno.app (one wildcard level). Hostnames with more than one subdomain level — all Pattern B hostnames and Pattern A staging hostnames — require Cloudflare for SaaS (CF4SaaS) per-hostname certificates.
| Hostname pattern | Example | Certificate mechanism |
|---|---|---|
*.nno.app | api.nno.app | Cloudflare Universal SSL |
*.stg.nno.app | gateway.svc.stg.nno.app | CF4SaaS per-hostname |
*.*.<pid>.nno.app | auth.svc.default.a1b2c3d4e5.nno.app | CF4SaaS per-hostname |
*.stg.*.<pid>.nno.app | auth.svc.stg.default.a1b2c3d4e5.nno.app | CF4SaaS per-hostname |
| Custom domains | app.acmecorp.com | CF4SaaS per-hostname |
All multi-level subdomains — whether NNO core staging or any client platform hostname — use CF4SaaS per-hostname certificates provisioned automatically by the NNO Provisioning Service when a resource is created.
7. DNS Mechanism Types
Two distinct DNS mechanisms operate under the nno.app namespace. Understanding which applies to which context avoids misconfiguration.
| Mechanism | Used By | How Configured | DNS Management | SSL |
|---|---|---|---|---|
| Workers Custom Domains | NNO Core (Pattern A) | [[routes]] + custom_domain = true in wrangler.toml | Cloudflare auto-manages DNS records on deploy | Automatic via Workers |
| CF4SaaS Custom Hostnames | Client Platform (Pattern B) | Programmatic via CF API in Provisioning service | Provisioning service registers via registerDns() | DV cert via HTTP validation, polled by ssl-poller |
NNO Core Services — Workers Custom Domains
For any NNO Core service, DNS is fully declarative. Add a [[routes]] block to the service's wrangler.toml and deploy — Cloudflare creates the DNS record automatically:
[[routes]]
pattern = "myservice.svc.nno.app"
custom_domain = true
[[env.stg.routes]]
pattern = "myservice.svc.stg.nno.app"
custom_domain = trueNo API calls, no polling, no registry entries. Wrangler handles everything.
Client Platform — CF4SaaS Custom Hostnames
Client platform hostnames require runtime provisioning because they are created dynamically per platform and stack. The Provisioning service at services/provisioning/src/dns/register.ts handles this:
- Builds prod + stg hostnames via
buildHostname()from@neutrino-io/core/naming - Registers both as CF4SaaS custom hostnames (idempotent — skips if already exists)
- Records both in the Registry
dns_recordstable with their CF hostname IDs
The registerDns() function is called during the BOOTSTRAP_PLATFORM provisioning job and whenever a new resource (Worker or Pages deployment) is added to a stack.
8. Custom Domains
Clients can map their own domains to any platform hostname using CF4SaaS.
Provisioning flow:
- NNO creates a CF4SaaS custom hostname record pointing to the target platform hostname
- Client adds a CNAME record in their DNS:
app.acmecorp.com CNAME auth.svc.default.<pid>.nno.app - Cloudflare validates ownership via HTTP challenge
- CF4SaaS provisions a DV TLS certificate for the custom domain automatically
- NNO Registry records the custom hostname in the
custom_domainstable linked to the platform resource
Registry Schema
Two Registry tables track DNS state:
dns_records — every hostname provisioned by NNO for a platform resource:
| Column | Description |
|---|---|
id | Record ID |
platformId | Owning platform |
stackId | Stack the resource belongs to |
hostname | Full *.nno.app hostname |
targetType | worker or pages |
resourceId | ID of the CF resource |
environment | prod or stg |
cfHostnameId | CF4SaaS custom hostname ID |
status | pending → active / failed |
sslStatus | pending → active / failed |
custom_domains — client-provided domain mappings:
| Column | Description |
|---|---|
id | Record ID |
platformId | Owning platform |
dnsRecordId | FK to dns_records (the target NNO hostname) |
hostname | Client's custom domain (e.g. app.acmecorp.com) |
targetDns | The *.nno.app CNAME target |
cfHostnameId | CF4SaaS custom hostname ID |
status | pending → active / failed |
sslStatus | pending → active / failed |
SSL Verification Lifecycle
SSL status flows: pending → active (verified) or failed (verification errors).
The ssl-poller cron (services/provisioning/src/cron/ssl-poller.ts) runs every 15 minutes. It fetches all pending custom domain records from the Registry, queries the CF4SaaS API for each cfHostnameId, and patches the record to active when ssl.status === "active" or to failed when verification_errors are present.
See registry.md for the full schema and provisioning.md for the provisioning job details.
9. Naming Utilities
All hostname construction and parsing is centralised in @neutrino-io/core/naming. Never construct DNS hostnames manually.
import {
buildHostname,
buildNnoHostname,
parseHostname,
isValidUserStackId,
} from '@neutrino-io/core/naming';
// Build a client platform hostname
buildHostname({
name: 'auth',
type: 'svc',
stackId: 'default',
platformId: 'a1b2c3d4e5',
environment: 'prod',
});
// => 'auth.svc.default.a1b2c3d4e5.nno.app'
// Build an NNO core hostname
buildNnoHostname({ name: 'gateway', type: 'svc', environment: 'prod' });
// => 'gateway.svc.nno.app'
// Parse any NNO hostname
parseHostname('auth.svc.default.a1b2c3d4e5.nno.app');
// => { pattern: 'B', name: 'auth', type: 'svc', stackId: 'default', platformId: 'a1b2c3d4e5', environment: 'prod' }
// Check if a stack-id value is valid for use in DNS
isValidUserStackId('default'); // => false (reserved)
isValidUserStackId('x7y8z9w0q1'); // => trueExports from @neutrino-io/core/naming:
| Export | Purpose |
|---|---|
buildHostname | Construct a Pattern B client platform hostname |
buildNnoHostname | Construct a Pattern A NNO core hostname |
parseHostname | Parse any *.nno.app hostname into its components |
isValidUserStackId | Check that a stack-id is not a reserved DNS keyword (default) |
generateId | Generate a Cloudflare-safe NanoID (10-char [a-z0-9]) |
buildResourceName | Build a Cloudflare resource name (\{pid\}-\{stackId\}-\{service\}[-stg]) |
buildNnoResourceName | Build an NNO-level resource name (nno-\{pid\}-\{service\}[-stg]) |
Related docs:
- cloudflare-naming.md — Cloudflare resource naming convention (Workers, D1, R2, KV)
- registry.md — DNS records and custom domains schema
- auth.md — Cross-origin cookie configuration
- provisioning.md — DNS registration in provisioning flows