NNO Registry
Documentation for NNO Registry
Date: 2026-03-30
Status: Detailed Design
Parent: System Architecture
Service: services/registry
Database: Cloudflare D1 (nno-k3m9p2xw7q-registry-db-\{env\})
Overview
The NNO Registry is the single source of truth for everything Neutrino manages. It tracks the full entity hierarchy (platforms, tenants, sub-tenants), every provisioned Cloudflare resource, the active feature set per entity, secret status, repo and build state, and an immutable audit log.
All other NNO services — Provisioning, CLI Service, Billing, IAM — read from and write to the Registry via its API. No service maintains its own state store for registry-class data.
NNO Gateway
│
▼
NNO Registry Service (Hono Worker)
│
▼
NNO Registry D1 (SQLite @ edge)1. D1 Schema [Phase 1]
1.1 platforms
One row per client platform onboarded to Neutrino.
CREATE TABLE platforms (
id TEXT PRIMARY KEY, -- NanoID (10 chars, [a-z0-9])
name TEXT NOT NULL, -- Display name. e.g. 'AcmeCorp'
slug TEXT NOT NULL UNIQUE, -- URL-safe slug. e.g. 'acmecorp'
status TEXT NOT NULL DEFAULT 'active',
-- 'pending' | 'provisioning' | 'active' | 'suspended' | 'pending_cancellation' | 'cancelled' | 'deleted'
tier TEXT NOT NULL DEFAULT 'starter',
-- 'starter' | 'growth' | 'scale'
cf_account_id TEXT, -- Cloudflare account ID (NULL = NNO shared account)
repo_name TEXT, -- GitHub repo name in NNO org. e.g. 'nno-platform-k3m9p2xw7q'
stripe_customer_id TEXT, -- Stripe customer ID (set during onboarding)
owner_user_id TEXT, -- References an IAM user ID (cross-service reference to Better Auth user). Identifies the platform owner who registered via self-serve onboarding.
cancelled_at INTEGER, -- Unix ms — when the platform was cancelled
cancellation_reason TEXT, -- Reason provided at cancellation
suspended_at INTEGER, -- Unix ms — when the platform was suspended
suspension_reason TEXT, -- Reason provided at suspension
trial_ends_at INTEGER, -- Unix ms — end of trial period (NULL = no trial)
created_at INTEGER NOT NULL, -- Unix ms
updated_at INTEGER NOT NULL,
deleted_at INTEGER -- Soft delete
);
CREATE INDEX idx_platforms_slug ON platforms(slug);
CREATE INDEX idx_platforms_status ON platforms(status);1.2 entities
Covers tenants and sub-tenants. The same table handles both via type and parent_id.
CREATE TABLE entities (
id TEXT PRIMARY KEY, -- NanoID (10 chars)
platform_id TEXT NOT NULL, -- FK → platforms.id
parent_id TEXT, -- NULL = top-level tenant; otherwise = sub-tenant
type TEXT NOT NULL, -- 'tenant' | 'subtenant'
name TEXT NOT NULL,
slug TEXT NOT NULL, -- Unique within a platform
status TEXT NOT NULL DEFAULT 'active',
-- 'active' | 'suspended' | 'deleted'
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
FOREIGN KEY (platform_id) REFERENCES platforms(id),
FOREIGN KEY (parent_id) REFERENCES entities(id)
);
CREATE UNIQUE INDEX idx_entities_platform_slug ON entities(platform_id, slug);
CREATE INDEX idx_entities_platform ON entities(platform_id);
CREATE INDEX idx_entities_parent ON entities(parent_id);Hierarchy queries (recursive CTE):
-- Get full ancestor chain for a given entity
WITH RECURSIVE ancestors AS (
SELECT id, parent_id, name, type, 0 AS depth
FROM entities WHERE id = ?
UNION ALL
SELECT e.id, e.parent_id, e.name, e.type, a.depth + 1
FROM entities e
JOIN ancestors a ON e.id = a.parent_id
)
SELECT * FROM ancestors ORDER BY depth DESC;
-- Get all descendants of a tenant
WITH RECURSIVE descendants AS (
SELECT id, parent_id, name, type, 0 AS depth
FROM entities WHERE id = ?
UNION ALL
SELECT e.id, e.parent_id, e.name, e.type, d.depth + 1
FROM entities e
JOIN descendants d ON e.parent_id = d.id
)
SELECT * FROM descendants ORDER BY depth;1.3 resources
Every Cloudflare resource NNO has provisioned, one row per resource.
CREATE TABLE resources (
id TEXT PRIMARY KEY, -- NanoID (10 chars)
platform_id TEXT NOT NULL, -- FK → platforms.id
entity_id TEXT NOT NULL, -- FK → entities.id (owner)
stack_id TEXT NOT NULL, -- FK → stacks.id (added migration 0009, made NOT NULL in 0011)
resource_type TEXT NOT NULL, -- 'worker' | 'pages' | 'd1' | 'r2' | 'kv' | 'queue'
service_name TEXT NOT NULL, -- Feature slug. e.g. 'auth', 'analytics'
environment TEXT NOT NULL, -- 'dev' | 'stg' | 'prod'
cf_name TEXT NOT NULL UNIQUE, -- Cloudflare resource name (the NNO naming convention output)
cf_id TEXT, -- Cloudflare internal ID (returned by CF API on creation)
cf_url TEXT, -- Worker/Pages URL (set after deploy)
status TEXT NOT NULL DEFAULT 'pending',
-- 'pending' | 'provisioning' | 'active' | 'failed' | 'deleted'
provision_job_id TEXT, -- FK → provision_jobs.id (last provisioning job)
config TEXT, -- JSON: resource-specific config (database_id, bucket_name, etc.)
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
deleted_at INTEGER,
FOREIGN KEY (platform_id) REFERENCES platforms(id),
FOREIGN KEY (entity_id) REFERENCES entities(id),
FOREIGN KEY (stack_id) REFERENCES stacks(id)
);
CREATE INDEX idx_resources_platform ON resources(platform_id);
CREATE INDEX idx_resources_entity ON resources(entity_id);
CREATE INDEX idx_resources_stack ON resources(stack_id);
CREATE INDEX idx_resources_cf_name ON resources(cf_name);
CREATE INDEX idx_resources_status ON resources(status);
CREATE INDEX idx_resources_type_env ON resources(resource_type, environment);config JSON shapes per resource type:
// worker
{ "script_tag": "v1.2.3", "last_deployed_at": 1234567890 }
// pages
{ "project_name": "k3m9p2xw7q-r8n4t6y1z5-portal-prod", "production_url": "https://..." }
// d1
{ "database_id": "fa098e4d-...", "migration_version": 5 }
// r2
{ "bucket_name": "k3m9p2xw7q-r8n4t6y1z5-media-prod", "location_hint": "apac" }
// kv
{ "namespace_id": "abc123...", "title": "k3m9p2xw7q-r8n4t6y1z5-cache-prod" }
// queue
{ "queue_id": "def456...", "consumers": [] }1.4 feature_activations
Tracks which features are active per entity and environment.
CREATE TABLE feature_activations (
id TEXT PRIMARY KEY, -- NanoID
platform_id TEXT NOT NULL,
entity_id TEXT NOT NULL,
feature_id TEXT NOT NULL, -- Feature slug. e.g. 'analytics'
feature_version TEXT NOT NULL, -- Semver of activated version
environment TEXT NOT NULL, -- 'dev' | 'stg' | 'prod'
status TEXT NOT NULL DEFAULT 'activating',
-- 'activating' | 'active' | 'deactivating' | 'inactive' | 'failed'
stack_instance_id TEXT, -- Legacy column — FK target `stack_instances` dropped in migration 0012. Retained as nullable text for backward compatibility. NULL = standalone activation; non-null = was managed by a stack instance.
activated_at INTEGER, -- Timestamp when status reached 'active'
deactivated_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE (entity_id, feature_id, environment),
FOREIGN KEY (platform_id) REFERENCES platforms(id),
FOREIGN KEY (entity_id) REFERENCES entities(id)
);
CREATE INDEX idx_feature_activations_entity ON feature_activations(entity_id);
CREATE INDEX idx_feature_activations_feature ON feature_activations(feature_id);
CREATE INDEX idx_feature_activations_status ON feature_activations(status);
CREATE INDEX idx_feature_activations_stack ON feature_activations(stack_instance_id);1.5 secrets
Tracks secret names and their sync status per resource. Values are never stored here — they live exclusively in Cloudflare.
CREATE TABLE secrets (
id TEXT PRIMARY KEY,
resource_id TEXT NOT NULL, -- FK → resources.id
secret_name TEXT NOT NULL, -- e.g. 'AUTH_SECRET', 'CORS_ORIGINS'
status TEXT NOT NULL DEFAULT 'missing',
-- 'missing' | 'set' | 'rotated' | 'error'
last_set_at INTEGER,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE (resource_id, secret_name),
FOREIGN KEY (resource_id) REFERENCES resources(id)
);
CREATE INDEX idx_secrets_resource ON secrets(resource_id);
CREATE INDEX idx_secrets_status ON secrets(status);1.6 platform_build_state
Tracks the state of the CF Pages build for each platform per environment.
CREATE TABLE platform_build_state (
id TEXT PRIMARY KEY,
platform_id TEXT NOT NULL,
environment TEXT NOT NULL, -- 'stg' | 'prod'
repo_name TEXT NOT NULL, -- GitHub repo name
last_commit_sha TEXT,
cf_deployment_id TEXT, -- Cloudflare Pages deployment ID
build_status TEXT NOT NULL DEFAULT 'unknown',
-- 'unknown' | 'building' | 'success' | 'failed'
build_url TEXT, -- CF Pages deployment URL
triggered_by TEXT, -- 'feature_activation' | 'manual' | 'push'
triggered_at INTEGER,
completed_at INTEGER,
updated_at INTEGER NOT NULL,
UNIQUE (platform_id, environment),
FOREIGN KEY (platform_id) REFERENCES platforms(id)
);1.7 provision_jobs
Tracks individual provisioning operations at the Registry level (job metadata, input payloads, audit trail).
Note: The Provisioning Service (
services/provisioning) maintains its own separateprovisioning_jobstable in its own D1. These are two different tables: the Registry's table stores job metadata and request payloads (usingoperation,payload,retry_count,max_retries), while the Provisioning Service's table stores execution state (usingtype,steps,error,started_at,completed_at). They share the same job IDs but are not FK-linked across services.
CREATE TABLE provision_jobs (
id TEXT PRIMARY KEY, -- NanoID
platform_id TEXT NOT NULL,
entity_id TEXT NOT NULL,
operation TEXT NOT NULL, -- 'bootstrap_platform' | 'activate_feature' | 'deactivate_feature' | 'provision_platform' | 'deprovision_resource' | 'provision_stack' | 'deactivate_stack'
status TEXT NOT NULL DEFAULT 'pending',
-- 'pending' | 'running' | 'completed' | 'failed' | 'rolled_back'
payload TEXT NOT NULL, -- JSON: input parameters for the job
steps TEXT, -- JSON array: completed steps with results (for rollback)
error TEXT, -- Error message if failed
retry_count INTEGER NOT NULL DEFAULT 0,
max_retries INTEGER NOT NULL DEFAULT 3,
created_at INTEGER NOT NULL,
started_at INTEGER,
completed_at INTEGER,
updated_at INTEGER NOT NULL,
FOREIGN KEY (platform_id) REFERENCES platforms(id),
FOREIGN KEY (entity_id) REFERENCES entities(id)
);
CREATE INDEX idx_provision_jobs_platform ON provision_jobs(platform_id);
CREATE INDEX idx_provision_jobs_status ON provision_jobs(status);
CREATE INDEX idx_provision_jobs_created ON provision_jobs(created_at DESC);1.8 audit_log
Immutable append-only log of all state changes. Never updated or deleted.
CREATE TABLE audit_log (
id TEXT PRIMARY KEY,
platform_id TEXT NOT NULL,
actor_id TEXT NOT NULL, -- User or service that triggered the action
actor_type TEXT NOT NULL, -- 'user' | 'system' | 'cli'
action TEXT NOT NULL, -- e.g. 'platform.created', 'feature.activated', 'resource.deleted'
entity_type TEXT NOT NULL, -- 'platform' | 'entity' | 'resource' | 'feature_activation'
entity_id TEXT NOT NULL, -- ID of the affected record
before TEXT, -- JSON snapshot before change (NULL for creates)
after TEXT, -- JSON snapshot after change (NULL for deletes)
metadata TEXT, -- JSON: additional context
actor_email TEXT, -- Email of the acting user (denormalized for quick display)
ip_address TEXT, -- Request IP address
user_agent TEXT, -- Request User-Agent header
created_at INTEGER NOT NULL
);
CREATE INDEX idx_audit_log_platform ON audit_log(platform_id);
CREATE INDEX idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX idx_audit_log_created ON audit_log(created_at DESC);
CREATE INDEX idx_audit_log_action ON audit_log(action);1.9 stack_instances (DROPPED)
Dropped in migration 0012. The
stack_instancestable (and its junction tablestack_feature_activations) were dropped in migration0012_drop_stack_instances. Stack management now uses thestackstable (see 1.10).
1.10 stacks
Tracks stacks as first-class registry entities. Each stack is a named project within a platform (Platform → Tenant → Stack hierarchy). Introduced in migration 0007_add_stacks.
CREATE TABLE stacks (
id TEXT PRIMARY KEY, -- NanoID (10 chars, [a-z0-9])
tenant_id TEXT NOT NULL, -- FK → entities.id (owning tenant)
platform_id TEXT NOT NULL, -- FK → platforms.id
name TEXT NOT NULL, -- Human display name, e.g. 'Marketing'
is_default INTEGER NOT NULL DEFAULT 0, -- 1 = default stack (auto-created per tenant)
template_id TEXT, -- Stack template ID (NULL = custom stack)
repo_name TEXT, -- GitHub repo name (created from nno-stack-starter)
status TEXT NOT NULL DEFAULT 'active',
-- 'active' | 'deleted'
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (tenant_id) REFERENCES entities(id),
FOREIGN KEY (platform_id) REFERENCES platforms(id)
);
CREATE INDEX idx_stacks_tenant ON stacks(tenant_id);
CREATE INDEX idx_stacks_platform ON stacks(platform_id);
CREATE INDEX idx_stacks_platform_tenant ON stacks(platform_id, tenant_id);
CREATE UNIQUE INDEX idx_stacks_platform_default ON stacks(platform_id, is_default) WHERE is_default = 1;Note: The doc previously used
entity_id— the actual column istenant_id. There is noslugordeleted_atcolumn. The unique constraint is a partial unique index on(platform_id, is_default)whereis_default = 1, ensuring at most one default stack per platform.
1.11 dns_records
Tracks all DNS hostnames registered via CF4SaaS for platform resources. Introduced in migration 0008_add_dns_records.
CREATE TABLE dns_records (
id TEXT PRIMARY KEY, -- NanoID
platform_id TEXT NOT NULL, -- FK → platforms.id
stack_id TEXT NOT NULL, -- FK → stacks.id
hostname TEXT NOT NULL UNIQUE, -- Full hostname, e.g. 'auth.svc.default.a1b2c3d4e5.nno.app'
target_type TEXT NOT NULL, -- 'nno_managed' | 'custom_domain'
resource_id TEXT NOT NULL, -- FK → resources.id (the CF resource this hostname routes to)
environment TEXT NOT NULL, -- 'dev' | 'stg' | 'prod'
cf_route_id TEXT, -- Cloudflare route ID (set after route creation)
status TEXT NOT NULL DEFAULT 'pending',
-- 'pending' | 'active' | 'failed' | 'deleted'
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (platform_id) REFERENCES platforms(id),
FOREIGN KEY (stack_id) REFERENCES stacks(id),
FOREIGN KEY (resource_id) REFERENCES resources(id)
);
CREATE INDEX idx_dns_platform ON dns_records(platform_id);
CREATE INDEX idx_dns_stack ON dns_records(stack_id);
CREATE INDEX idx_dns_status ON dns_records(status);Note: The column is
target_type(nottypeas in earlier docs).stack_idandresource_idare NOT NULL. There is nossl_status,validation_records,cf_hostname_id, ordeleted_atcolumn — SSL/validation fields live oncustom_domainsinstead.cf_route_idwas added for Cloudflare Workers route binding.
1.12 custom_domains
Tracks client-provided custom domains mapped to platform resources. SSL and verification fields live here (not on dns_records).
CREATE TABLE custom_domains (
id TEXT PRIMARY KEY, -- NanoID
platform_id TEXT NOT NULL, -- FK → platforms.id
dns_record_id TEXT NOT NULL, -- FK → dns_records.id
hostname TEXT NOT NULL UNIQUE, -- Client's domain, e.g. 'app.acmecorp.com'
target_dns TEXT NOT NULL, -- NNO hostname it resolves to
cf_hostname_id TEXT, -- CF4SaaS custom hostname ID
ssl_status TEXT DEFAULT 'pending', -- CF4SaaS SSL status: 'pending' | 'active' | 'expired'
status TEXT NOT NULL DEFAULT 'pending',
-- 'pending' | 'active' | 'failed' | 'deleted'
verified_at INTEGER, -- Unix ms — when domain ownership was verified
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY (platform_id) REFERENCES platforms(id),
FOREIGN KEY (dns_record_id) REFERENCES dns_records(id)
);
CREATE INDEX idx_custom_domains_platform ON custom_domains(platform_id);
CREATE INDEX idx_custom_domains_status ON custom_domains(status);
CREATE INDEX idx_custom_domains_dns_record ON custom_domains(dns_record_id);Note: The column is
hostname(notcustom_hostname) andtarget_dns(nottarget_hostname). SSL-related fields (ssl_status,cf_hostname_id) andverified_atlive on this table, not ondns_records.
1.13 resources — stack_id column
The resources table (see 1.3) includes a stack_id column. Originally added as nullable in migration 0009_add_resources_stack_id, it was made NOT NULL in migration 0011_make_stack_id_not_null. Resources are associated with a stack within the Platform → Tenant → Stack hierarchy.
stack_id TEXT NOT NULL REFERENCES stacks(id)The entity_id column is retained for backward compatibility but new resources populate stack_id. An idx_resources_stack index exists on this column.
1.14 platform_lifecycle_events
Tracks every platform status transition (e.g. pending → provisioning → active, or active → suspended). Introduced in migration 0004_platform_lifecycle.
CREATE TABLE platform_lifecycle_events (
id TEXT PRIMARY KEY,
platform_id TEXT NOT NULL,
from_status TEXT NOT NULL, -- Previous status
to_status TEXT NOT NULL, -- New status
triggered_by TEXT NOT NULL, -- Actor ID (user or system)
trigger_type TEXT NOT NULL, -- 'user_action' | 'system' | 'billing_event'
reason TEXT, -- Optional reason for the transition
metadata TEXT, -- JSON: additional context
created_at INTEGER NOT NULL,
FOREIGN KEY (platform_id) REFERENCES platforms(id)
);
CREATE INDEX idx_lifecycle_platform ON platform_lifecycle_events(platform_id);
CREATE INDEX idx_lifecycle_created ON platform_lifecycle_events(created_at);1.15 onboarding_sessions
Tracks self-serve client onboarding progress as a step-by-step checklist. Introduced in migration 0005_onboarding_sessions, with expiry support added in 0006_onboarding_sessions_expiry.
CREATE TABLE onboarding_sessions (
id TEXT PRIMARY KEY,
platform_id TEXT NOT NULL,
user_id TEXT NOT NULL, -- IAM user performing the onboarding
status TEXT NOT NULL DEFAULT 'in_progress',
-- 'in_progress' | 'completed' | 'expired' | 'failed'
steps TEXT NOT NULL, -- JSON: array of step objects with completion status
stack_template_id TEXT, -- Selected stack template (NULL = custom)
tier TEXT NOT NULL, -- Platform tier at time of onboarding
provision_job_id TEXT, -- FK → provision_jobs.id (linked provisioning job)
current_step TEXT, -- Current step identifier
metadata TEXT, -- JSON: additional onboarding context
started_at INTEGER NOT NULL,
completed_at INTEGER,
expires_at INTEGER, -- Unix ms — session expiry (added in migration 0006)
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
CREATE INDEX idx_onboarding_platform ON onboarding_sessions(platform_id);
CREATE INDEX idx_onboarding_user ON onboarding_sessions(user_id);
CREATE INDEX idx_onboarding_status ON onboarding_sessions(status);
CREATE INDEX idx_onboarding_expires ON onboarding_sessions(expires_at);1.16 stack_feature_activations (DROPPED)
Dropped in migration 0012. The
stack_feature_activationsjunction table was dropped alongsidestack_instancesin migration0012_drop_stack_instances.
2. API Contract [Phase 1]
All endpoints are served by the NNO Registry Worker via the NNO Gateway. Base path: /api/v1.
Authentication: Authorization: Bearer \{nno-session-token\} on all requests.
2.1 Platforms
GET /platforms List platforms (paginated)
POST /platforms Create platform
GET /platforms/:platformId Get platform by ID
PATCH /platforms/:platformId Update platform (name, tier, status)
DELETE /platforms/:platformId Soft-delete platformPOST /platforms
// Request
{
"name": "AcmeCorp",
"slug": "acmecorp",
"tier": "starter"
}
// Response 201
{
"id": "k3m9p2xw7q",
"name": "AcmeCorp",
"slug": "acmecorp",
"status": "active",
"tier": "starter",
"createdAt": "2026-02-22T10:00:00Z"
}2.2 Entities (Tenants & Sub-Tenants)
GET /platforms/:platformId/entities List entities (with optional ?type=tenant|subtenant)
POST /platforms/:platformId/entities Create entity
GET /platforms/:platformId/entities/:entityId Get entity
PATCH /platforms/:platformId/entities/:entityId Update entity
DELETE /platforms/:platformId/entities/:entityId Soft-delete entity
GET /platforms/:platformId/entities/:entityId/ancestors Full ancestor chain
GET /platforms/:platformId/entities/:entityId/descendants All descendantsPOST /platforms/:platformId/entities
// Request
{
"name": "Team Alpha",
"slug": "team-alpha",
"type": "tenant",
"parentId": null // null for top-level tenant, entityId for sub-tenant
}
// Response 201
{
"id": "r8n4t6y1z5",
"platformId": "k3m9p2xw7q",
"parentId": null,
"type": "tenant",
"name": "Team Alpha",
"slug": "team-alpha",
"status": "active",
"createdAt": "2026-02-22T10:01:00Z"
}2.3 Resources
GET /platforms/:platformId/resources List all resources for a platform
GET /platforms/:platformId/entities/:entityId/resources List resources for an entity
POST /platforms/:platformId/entities/:entityId/resources Register a resource (called by Provisioning)
GET /platforms/:platformId/resources/:resourceId Get resource
PATCH /platforms/:platformId/resources/:resourceId Update resource (status, config, cf_id)
DELETE /platforms/:platformId/resources/:resourceId Mark deleted
GET /resources/lookup?cfName=k3m9p2xw7q-r8n4t6y1z5-auth-prod Lookup by CF namePATCH /platforms/:platformId/resources/:resourceId — used by Provisioning to update status:
// Request
{
"status": "active",
"cfId": "fa098e4d-8baf-4cad-b93a-4570cb9c3886",
"cfUrl": "https://k3m9p2xw7q-r8n4t6y1z5-auth-prod.workers.dev",
"config": { "database_id": "fa098e4d-8baf-4cad-b93a-4570cb9c3886" }
}2.4 Feature Activations
GET /platforms/:platformId/entities/:entityId/features List active features
POST /platforms/:platformId/entities/:entityId/features/activate Activate a feature
POST /platforms/:platformId/entities/:entityId/features/deactivate Deactivate a feature
GET /platforms/:platformId/entities/:entityId/features/:featureId Get activation statusPOST /features/activate
// Request
{
"featureId": "analytics",
"version": "1.2.0",
"environment": "prod"
}
// Response 202 (async — provisioning triggered)
{
"activationId": "act_m7p2x9k1q4",
"featureId": "analytics",
"status": "activating",
"provisionJobId": "job_n3r8t5w2y6",
"estimatedDurationSeconds": 30
}2.5 Feature Manifest (Shell Boot)
The shell fetches this endpoint at boot to know which features to load:
GET /platforms/:platformId/entities/:entityId/manifest?env=prod// Response 200
{
"platformId": "k3m9p2xw7q",
"entityId": "r8n4t6y1z5",
"environment": "prod",
"features": [
{
"id": "auth",
"version": "1.0.0",
"package": "@neutrino-io/ui-auth",
"serviceUrl": "https://k3m9p2xw7q-r8n4t6y1z5-auth-prod.workers.dev",
"status": "active"
},
{
"id": "analytics",
"version": "1.2.0",
"package": "@acme/ui-analytics",
"serviceUrl": "https://k3m9p2xw7q-r8n4t6y1z5-analytics-prod.workers.dev",
"status": "active"
}
],
"generatedAt": "2026-02-22T10:00:00Z"
}In Phase 1 (static bundling), this manifest is baked into the shell at build time via
features.config.ts. In Phase 2 (remote federation), the shell fetches this at runtime.
2.6 Secrets
GET /platforms/:platformId/resources/:resourceId/secrets List secret names + status
POST /platforms/:platformId/resources/:resourceId/secrets/:secretName Mark secret as set/missing2.7 Build State
GET /platforms/:platformId/builds List build history
GET /platforms/:platformId/builds/latest?env=prod Get latest build state
PATCH /platforms/:platformId/builds/:buildId Update build status (called by CLI Service)2.8 Audit Log
GET /platforms/:platformId/audit?entity=:entityId&action=feature.activated&limit=50&cursor=:cursor2.9 Stack Instances [Phase 1]
Legacy Naming: The Stack Registry service D1 databases use the legacy prefix
marketplace-db-*(e.g.,nno-k3m9p2xw7q-marketplace-db-stg) rather thanstack-registry-db-*. This predates the service rename from "marketplace" to "stack-registry." Thewrangler.tomlbinding names are unchanged to avoid requiring a data migration. New stack resources follow the standard convention.
GET /platforms/:platformId/stacks List stack instances for a platform
POST /platforms/:platformId/stacks Activate a stack template (or create local stack)
GET /platforms/:platformId/stacks/:stackInstanceId Stack instance detail + feature activation list
PATCH /platforms/:platformId/stacks/:stackInstanceId Update status / shared_resources (called by Provisioning)
DELETE /platforms/:platformId/stacks/:stackInstanceId Deactivate a stackPOST /platforms/:platformId/stacks — activate a stack template:
// Request (template-based)
{
"templateName": "saas-starter",
"templateVersion": "1.0.0",
"environment": "prod"
}
// Request (platform-local)
{
"stackName": "my-custom-stack",
"features": [
{ "featureId": "billing", "required": true },
{ "featureId": "zero", "required": false }
],
"resources": { "sharedD1": true },
"isLocal": true,
"environment": "prod"
}
// Response 202
{
"stackInstanceId": "si_k3m9p2xw7q",
"jobId": "job_n3r8t5w2y6",
"status": "pending"
}GET /platforms/:platformId/stacks/:stackInstanceId response:
{
"id": "si_k3m9p2xw7q",
"templateId": "tpl_saas_starter",
"stackName": "saas-starter",
"version": "1.0.0",
"status": "active",
"isLocal": false,
"sharedResources": {
"d1Id": "fa098e4d-...",
"kvId": "abc123..."
},
"features": [
{ "featureId": "billing", "status": "active", "activatedAt": "2026-02-28T10:00:00Z" },
{ "featureId": "settings", "status": "active", "activatedAt": "2026-02-28T10:00:00Z" },
{ "featureId": "analytics","status": "skipped","activatedAt": null }
],
"activatedAt": "2026-02-28T10:00:00Z"
}2.10 Onboarding [Phase 1]
| Method | Path | Description |
|---|---|---|
POST | /api/v1/onboarding | Create onboarding session — generates session ID, inserts default 5-step pipeline, optionally triggers provisioning job via NNO_PROVISIONING service binding |
GET | /api/v1/onboarding/:onboardingId | Fetch onboarding session by ID |
PATCH | /api/v1/onboarding/:onboardingId | Update session steps, status, or provisionJobId |
Default Steps: create_platform, provision_infra, setup_repo, deploy_services, verify
Request Schema (POST):
platformName(string, required)slug(string, required)stackTemplateId(string, optional)tier(string, required)userId(string, required)
See services/onboarding.md for the full pipeline blueprint.
3. Pagination [Phase 1]
All list endpoints use cursor-based pagination (not offset). D1 query performance degrades with large OFFSET values; cursors remain O(log n) regardless of position.
Request
GET /platforms/:platformId/resources?limit=25&cursor=eyJpZCI6ImszOW0ifQ==| Parameter | Default | Max | Description |
|---|---|---|---|
limit | 25 | 100 | Records per page |
cursor | (none) | — | Opaque cursor from previous response |
Response Envelope
{
"data": [ ...records ],
"pagination": {
"hasMore": true,
"nextCursor": "eyJpZCI6ImszOW0ifQ==",
"total": null // total count omitted by default (expensive on D1); request with ?count=true
}
}Cursor Implementation
Cursors encode the last seen id and created_at for stable ordering across inserts:
// Encode
const cursor = btoa(JSON.stringify({ id: lastRecord.id, createdAt: lastRecord.created_at }));
// Decode and apply in query
const { id, createdAt } = JSON.parse(atob(cursor));
// D1 query with cursor
db.prepare(`
SELECT * FROM resources
WHERE platform_id = ?
AND (created_at, id) < (?, ?)
ORDER BY created_at DESC, id DESC
LIMIT ?
`).bind(platformId, createdAt, id, limit);4. Error Response Format [Phase 1]
All errors follow a consistent shape:
// 4xx / 5xx
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Resource 'k3m9p2xw7q-r8n4t6y1z5-auth-prod' not found",
"details": {},
"requestId": "req_7x2m9k1p4q"
}
}| HTTP Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | Request body fails schema validation |
| 401 | UNAUTHORIZED | Missing or invalid bearer token |
| 403 | FORBIDDEN | Token valid but insufficient permissions for this platform/entity |
| 404 | RESOURCE_NOT_FOUND | Requested record does not exist |
| 409 | CONFLICT | Duplicate slug, resource already exists |
| 422 | UNPROCESSABLE | Request valid but action cannot be completed (e.g. activate already-active feature) |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Unexpected server error |
5. Key Query Patterns [Phase 1]
Get all active features for an entity across all environments
SELECT fa.*, r.cf_url, r.status AS resource_status
FROM feature_activations fa
LEFT JOIN resources r
ON r.entity_id = fa.entity_id
AND r.service_name = fa.feature_id
AND r.environment = fa.environment
WHERE fa.entity_id = ?
AND fa.status = 'active'
ORDER BY fa.feature_id, fa.environment;Get full resource inventory for a platform
SELECT
e.name AS entity_name,
e.type AS entity_type,
r.resource_type,
r.service_name,
r.environment,
r.cf_name,
r.status
FROM resources r
JOIN entities e ON r.entity_id = e.id
WHERE r.platform_id = ?
AND r.deleted_at IS NULL
ORDER BY e.name, r.service_name, r.environment;Check for missing secrets before a deployment
SELECT r.cf_name, s.secret_name
FROM secrets s
JOIN resources r ON s.resource_id = r.id
WHERE r.platform_id = ?
AND r.environment = ?
AND s.status = 'missing'
ORDER BY r.cf_name, s.secret_name;Get provision job status with steps
SELECT
pj.*,
COUNT(r.id) AS resources_created
FROM provision_jobs pj
LEFT JOIN resources r ON r.provision_job_id = pj.id
WHERE pj.id = ?
GROUP BY pj.id;Status: Detailed design — stack_instances and stack_feature_activations tables added 2026-02-28
Implementation target: services/registry/
Related: NNO Provisioning · System Architecture · Stacks