NNO Docs
ArchitectureCross cutting

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          # staging

NNO Core Service Hostnames

ServiceProductionStaging
Console (operator UI)console.app.nno.appconsole.app.stg.nno.app
Gatewaygateway.svc.nno.appgateway.svc.stg.nno.app
IAMiam.svc.nno.appiam.svc.stg.nno.app
Registryregistry.svc.nno.appregistry.svc.stg.nno.app
Billingbilling.svc.nno.appbilling.svc.stg.nno.app
Provisioningprovisioning.svc.nno.appprovisioning.svc.stg.nno.app
CLI Servicecli.svc.nno.appcli.svc.stg.nno.app
Stack Registrystack-registry.svc.nno.appstack-registry.svc.stg.nno.app
Documentationdocs.app.nno.appdoc.app.stg.nno.app

Public aliases (CNAME records):

  • api.nno.appgateway.svc.nno.app
  • api.stg.nno.appgateway.svc.stg.nno.app
  • console.nno.appconsole.app.nno.app (short alias for operator convenience)
  • docs.nno.appdocs.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):

EnvironmentPages ProjectCustom DomainBuild Mode
Productionnno-k3m9p2xw7q-consoleconsole.app.nno.app, console.nno.appbuild:prod (.env.prod)
Stagingnno-k3m9p2xw7q-console-stgconsole.app.stg.nno.appbuild: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.app could proxy to branch preview URLs (develop.\{project\}.pages.dev), eliminating the need for separate -stg projects. 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      # staging

Segment Definitions

SegmentDescriptionFormat
<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 typeapp (Pages — frontend/static) or svc (Worker — backend API)
stgStaging indicatorLiteral stg; absent in production
<stack-id>Stack identifierdefault (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 identifier10-char nano-id [a-z0-9]

Concrete Examples — Platform a1b2c3d4e5

Default stack (auth lives here, auto-created per tenant):

ResourceHostname
Auth Worker (prod)auth.svc.default.a1b2c3d4e5.nno.app
Auth Worker (staging)auth.svc.stg.default.a1b2c3d4e5.nno.app

Stack x7y8z9w0q1 (e.g. "Marketing"):

ResourceHostname
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 countPatternEnvironment
2Pattern A — NNO CoreProduction
3Pattern A — NNO CoreStaging (contains stg)
4Pattern B — Client PlatformProduction
5Pattern B — Client PlatformStaging (contains stg)

Rule: 2–3 segments = Pattern A; 4–5 segments = Pattern B. Odd count (3 or 5) = staging.


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.

EnvironmentShell OriginAuth OriginCookie DomainSameSiteSecure
Developmentlocalhost:5174localhost:8787localhostLaxfalse
Production<name>.app.<stack>.<pid>.nno.appauth.svc.default.<pid>.nno.app.<pid>.nno.appNonetrue
Staging<name>.app.stg.<stack>.<pid>.nno.appauth.svc.stg.default.<pid>.nno.app.<pid>.nno.appNonetrue

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 patternExampleCertificate mechanism
*.nno.appapi.nno.appCloudflare Universal SSL
*.stg.nno.appgateway.svc.stg.nno.appCF4SaaS per-hostname
*.*.<pid>.nno.appauth.svc.default.a1b2c3d4e5.nno.appCF4SaaS per-hostname
*.stg.*.<pid>.nno.appauth.svc.stg.default.a1b2c3d4e5.nno.appCF4SaaS per-hostname
Custom domainsapp.acmecorp.comCF4SaaS 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.

MechanismUsed ByHow ConfiguredDNS ManagementSSL
Workers Custom DomainsNNO Core (Pattern A)[[routes]] + custom_domain = true in wrangler.tomlCloudflare auto-manages DNS records on deployAutomatic via Workers
CF4SaaS Custom HostnamesClient Platform (Pattern B)Programmatic via CF API in Provisioning serviceProvisioning 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 = true

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

  1. Builds prod + stg hostnames via buildHostname() from @neutrino-io/core/naming
  2. Registers both as CF4SaaS custom hostnames (idempotent — skips if already exists)
  3. Records both in the Registry dns_records table 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:

  1. NNO creates a CF4SaaS custom hostname record pointing to the target platform hostname
  2. Client adds a CNAME record in their DNS: app.acmecorp.com CNAME auth.svc.default.<pid>.nno.app
  3. Cloudflare validates ownership via HTTP challenge
  4. CF4SaaS provisions a DV TLS certificate for the custom domain automatically
  5. NNO Registry records the custom hostname in the custom_domains table linked to the platform resource

Registry Schema

Two Registry tables track DNS state:

dns_records — every hostname provisioned by NNO for a platform resource:

ColumnDescription
idRecord ID
platformIdOwning platform
stackIdStack the resource belongs to
hostnameFull *.nno.app hostname
targetTypeworker or pages
resourceIdID of the CF resource
environmentprod or stg
cfHostnameIdCF4SaaS custom hostname ID
statuspendingactive / failed
sslStatuspendingactive / failed

custom_domains — client-provided domain mappings:

ColumnDescription
idRecord ID
platformIdOwning platform
dnsRecordIdFK to dns_records (the target NNO hostname)
hostnameClient's custom domain (e.g. app.acmecorp.com)
targetDnsThe *.nno.app CNAME target
cfHostnameIdCF4SaaS custom hostname ID
statuspendingactive / failed
sslStatuspendingactive / failed

SSL Verification Lifecycle

SSL status flows: pendingactive (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');  // => true

Exports from @neutrino-io/core/naming:

ExportPurpose
buildHostnameConstruct a Pattern B client platform hostname
buildNnoHostnameConstruct a Pattern A NNO core hostname
parseHostnameParse any *.nno.app hostname into its components
isValidUserStackIdCheck that a stack-id is not a reserved DNS keyword (default)
generateIdGenerate a Cloudflare-safe NanoID (10-char [a-z0-9])
buildResourceNameBuild a Cloudflare resource name (\{pid\}-\{stackId\}-\{service\}[-stg])
buildNnoResourceNameBuild an NNO-level resource name (nno-\{pid\}-\{service\}[-stg])

Related docs:

On this page