NNO Docs
ArchitectureServices

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 separate provisioning_jobs table in its own D1. These are two different tables: the Registry's table stores job metadata and request payloads (using operation, payload, retry_count, max_retries), while the Provisioning Service's table stores execution state (using type, 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_instances table (and its junction table stack_feature_activations) were dropped in migration 0012_drop_stack_instances. Stack management now uses the stacks table (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 is tenant_id. There is no slug or deleted_at column. The unique constraint is a partial unique index on (platform_id, is_default) where is_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 (not type as in earlier docs). stack_id and resource_id are NOT NULL. There is no ssl_status, validation_records, cf_hostname_id, or deleted_at column — SSL/validation fields live on custom_domains instead. cf_route_id was 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 (not custom_hostname) and target_dns (not target_hostname). SSL-related fields (ssl_status, cf_hostname_id) and verified_at live on this table, not on dns_records.

1.13 resourcesstack_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. pendingprovisioningactive, or activesuspended). 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_activations junction table was dropped alongside stack_instances in migration 0012_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 platform

POST /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 descendants

POST /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 name

PATCH /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 status

POST /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/missing

2.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=:cursor

2.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 than stack-registry-db-*. This predates the service rename from "marketplace" to "stack-registry." The wrangler.toml binding 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 stack

POST /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]

MethodPathDescription
POST/api/v1/onboardingCreate onboarding session — generates session ID, inserts default 5-step pipeline, optionally triggers provisioning job via NNO_PROVISIONING service binding
GET/api/v1/onboarding/:onboardingIdFetch onboarding session by ID
PATCH/api/v1/onboarding/:onboardingIdUpdate 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==
ParameterDefaultMaxDescription
limit25100Records 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 StatusCodeWhen
400VALIDATION_ERRORRequest body fails schema validation
401UNAUTHORIZEDMissing or invalid bearer token
403FORBIDDENToken valid but insufficient permissions for this platform/entity
404RESOURCE_NOT_FOUNDRequested record does not exist
409CONFLICTDuplicate slug, resource already exists
422UNPROCESSABLERequest valid but action cannot be completed (e.g. activate already-active feature)
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORUnexpected 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

On this page