NNO Docs
ArchitectureServices

NNO CLI Service

Documentation for NNO CLI Service

Date: 2026-03-30 Status: Detailed Design Parent: System Architecture Package: services/cli


Overview

The NNO CLI Service is a Cloudflare Worker that acts as the backend for both the nno CLI binary and the NNO Portal's platform management actions.

Responsibility Boundary

ResponsibilityDescription
GitHub repo managementCreates per-platform repos from the nno-stack-starter template via the GitHub App. Manages features.config.ts, package.json, env.config.ts, and auth.config.ts file commits using the Git Data API (multi-file atomic commits).
Feature config commitsWhen a feature is activated/deactivated (Phase 2), commits updated features.config.ts to the platform repo. A single commit covers all changed files to avoid multiple CF Pages builds.
CF Pages build triggerA git push to the platform repo triggers a GitHub Actions workflow (deploy.yml) included in the nno-stack-starter template. The workflow runs pnpm build and wrangler pages deploy to push the built shell to CF Pages. This mirrors how apps/console deploys and avoids requiring a CF dashboard OAuth connection per platform. Direct CF Pages API calls are reserved for manual/emergency deploys only.
Build status trackingPolls and surfaces CF Pages deployment status back to callers via GET /platforms/\{id\}/build-status.
GitHub App token cachingCaches short-lived GitHub App installation tokens in KV (TOKENS binding) with a 50-minute TTL to avoid re-authenticating on every request.

The NNO CLI Service is not directly called by end users. It is called by:

  • nno deploy CLI command
  • NNO Provisioning Service (after feature infrastructure is ready)
  • NNO Portal (on admin actions)

Phase 1 Status

CapabilityStatusNotes
POST /platforms — Platform repo creation via GitHub API✅ LiveCreates repo from nno-stack-starter template; caches in KV
GET /platforms/\{platformId\}/repo — Repo info✅ LiveReads from KV cache
POST /platforms/\{platformId\}/features/activate — Feature activation✅ LiveReads/writes features.config.ts via GitHub Contents API
POST /platforms/\{platformId\}/features/deactivate — Feature deactivation✅ LiveSoft-disables feature in features.config.ts
POST /platforms/\{platformId\}/deploy — Manual CF Pages rebuild✅ LiveDirect CF Pages API trigger
GET /platforms/\{platformId\}/build-status — Build status✅ LivePolls CF Pages API
POST /platforms/\{platformId\}/secrets — Provision CF Pages env secrets✅ LivePatches CF Pages project via CF API
GitHub App JWT token caching (KV, 50-min TTL)✅ Live
Feature Activation Flow (end-to-end: Portal → Gateway → Provisioning → CLI Service → GitHub → CF Pages)🟡 PartialPer-service infra provisioning live; shell rebuild flow integrated

1. GitHub App Setup [Phase 1]

The NNO CLI Service authenticates to GitHub using a GitHub App (not a personal access token), providing:

  • Fine-grained permissions scoped to the NNO organisation
  • Per-installation tokens (short-lived, auto-refreshed)
  • Audit trail in the GitHub App installation logs

Required GitHub App Permissions

PermissionLevelReason
ContentsRead & WriteCreate/update files in platform repos
MetadataReadRepo info lookup
AdministrationRead & WriteCreate repos from template
WorkflowsRead & WriteRequired only if platform repos use GitHub Actions CI

Credential Storage

GitHub App credentials are stored as Wrangler secrets on the NNO CLI Service Worker:

SecretDescription
GITHUB_APP_IDGitHub App ID (numeric string)
GITHUB_APP_PRIVATE_KEYPEM-encoded RSA private key
GITHUB_APP_INSTALLATION_IDGitHub App installation ID for the NNO organisation
TEMPLATE_REPO_FULL_NAMEFull path of the template repo (e.g., neutrino-io/nno-stack-starter)

Token Generation

import { createAppAuth } from '@octokit/auth-app';
import { Octokit } from '@octokit/rest';

async function getOctokit(env: Env): Promise<Octokit> {
  const auth = createAppAuth({
    appId: env.GITHUB_APP_ID,
    privateKey: env.GITHUB_APP_PRIVATE_KEY,
  });

  // Get installation ID for the NNO org
  const { token } = await auth({
    type: 'installation',
    installationId: await getInstallationId(env),
  });

  return new Octokit({ auth: token });
}

Tokens are cached in KV with a TTL of 50 minutes (GitHub App tokens expire at 60 minutes).

Note: GitHub App JWT authentication is a Phase 2 feature. The current implementation uses a static GITHUB_APP_PRIVATE_KEY secret configured via wrangler secret put.


2. Platform Repo Structure [Phase 1]

Every platform gets its own GitHub repository in the NNO organisation:

neutrino-platform/
└── {platform-id}-platform-console/   ← one repo per platform
    ├── src/
    │   ├── config/
    │   │   ├── features.config.ts    ← NNO-managed: active feature list
    │   │   ├── auth.config.ts        ← NNO-managed: auth service URL
    │   │   └── env.config.ts         ← NNO-managed: per-tenant env vars
    │   └── ...                       ← shell source (from template, not modified)
    ├── package.json                  ← NNO-managed: feature package deps
    ├── pnpm-lock.yaml                ← regenerated on feature changes
    ├── wrangler.toml                 ← CF Pages config (NNO-managed)
    └── .github/
        └── workflows/
            └── ci.yml               ← optional CI (from template)

Files Managed by NNO CLI Service

FileWhen Updated
src/config/features.config.tsFeature activated / deactivated
package.jsonFeature activated / deactivated
pnpm-lock.yamlFeature activated / deactivated (regenerated via pnpm)
src/config/auth.config.tsPlatform provisioned / auth URL changed
src/config/env.config.tsService URL added/removed (on feature activation)
wrangler.tomlPlatform created / CF Pages project config change

All other files (shell source, vite.config.ts, components, etc.) come from the template and are not modified after repo creation. Platform source changes are out of scope for NNO CLI Service.

Template Repo (nno-stack-starter)

The nno-stack-starter template serves as the canonical template for per-stack repos. The CLI Service creates new stack repos using GitHub's Create repository from template API, then applies the initial stack-specific config overrides.


3. API Endpoints [Phase 1]

All endpoints require Authorization: Bearer \{nno-session-token\} and a valid NNO platform-admin or NNO-operator role.

3.1 POST /platforms — Create Platform Repo

Called by NNO Provisioning after a platform is registered in the Registry.

Request

{
  "platformId": "k3m9p2xw7q",
  "displayName": "AcmeCorp",
  "planTier": "growth"
}

Response201 Created

{
  "platformId": "k3m9p2xw7q",
  "repoFullName": "neutrino-platform/k3m9p2xw7q-platform-console",
  "status": "created"
}

Returns status: "exists" (200) if the platform repo was already created (idempotent).

What it does (Phase 1):

  1. Creates repo from template via GitHub API (neutrino-io/nno-stack-starter)
  2. Stores repo record in KV (repo:\{platformId\})
  3. Returns \{ platformId, repoFullName, status \}

3.2 POST /platforms/\{platformId\}/features/activate — Activate Feature

Called by NNO Provisioning after feature Worker + D1 are ready.

Request

{
  "featureId": "analytics",
  "version": "1.2.0",
  "config": {}
}

Response

{
  "status": "activated",
  "featureId": "analytics"
}

What it does:

  1. Validates the platform repo exists in KV
  2. Reads features.config.ts from the platform repo via GitHub Contents API
  3. Adds the feature entry (featureId, enabled: true, version, optional config)
  4. Commits the updated file back to the platform repo
  5. Returns \{ status: 'activated', featureId, version \}

3.3 POST /platforms/\{platformId\}/features/deactivate — Deactivate Feature

Request

{
  "featureId": "analytics"
}

Phase 2 schema additions: packageName (e.g., "@neutrino-io/ui-analytics") and tenantId will be added to the deactivate request body when the full commit-based workflow is implemented.

What it does:

  1. Fetches current features.config.ts and package.json
  2. Sets enabled: false on the feature entry (does not delete — preserves config history)
  3. Removes serviceUrl from env.config.ts
  4. Commits: "feat: deactivate analytics for tenant r8n4t6y1z5"
  5. Triggers CF Pages rebuild

Soft disable vs. hard remove: Features are soft-disabled (enabled: false) rather than removed from the config. Hard removal is only done on platform deprovision. This preserves config history and makes re-activation cheaper (no regeneration needed, just flip enabled: true).


3.4 POST /platforms/\{platformId\}/deploy — Trigger Manual Rebuild

Called by nno deploy CLI command or NNO Portal.

Request

{
  "environment": "stg",
  "reason": "Manual deploy triggered by operator"
}

Response

{
  "deploymentId": "01JK8M2P3N4Q5R6S7T8U9V0W1X",
  "url": "https://k3m9p2xw7q-console.pages.dev"
}

What it does:

  1. Calls Cloudflare Pages API: POST /accounts/\{accountId\}/pages/projects/\{projectName\}/deployments
  2. Returns the CF Pages deployment ID and preview URL

3.5 GET /platforms/\{platformId\}/build-status — Build Status

Response

{
  "platformId": "k3m9p2xw7q",
  "cfPagesProjectName": "k3m9p2xw7q-console",
  "latestDeployment": {
    "id": "01JK8M2P3N4Q5R6S7T8U9V0W1X",
    "status": "active",
    "url": "https://k3m9p2xw7q-console.pages.dev",
    "createdOn": "2026-02-22T10:34:00Z"
  }
}

latestDeployment is null if no deployments exist yet.

CF Pages deployment status values: queuedin_progressactive | failure | canceled


3.6 GET /platforms/\{platformId\}/repo — Repo Info

Returns the current state of the platform repo (URL, branch, last commit, installed features).


3.7 POST /platforms/\{platformId\}/secrets — Provision CF Pages Env Secrets

Called by NNO Provisioning after infrastructure secrets are ready (e.g., D1 binding IDs, Worker URLs).

Request

{
  "secrets": {
    "BILLING_WORKER_URL": "https://billing.nno.internal",
    "ANALYTICS_DB_ID": "abc123"
  }
}

Response

{
  "platformId": "k3m9p2xw7q",
  "results": {
    "BILLING_WORKER_URL": "ok",
    "ANALYTICS_DB_ID": "ok"
  }
}

What it does: Sets each key as a CF Pages environment variable (secret_text type) via PATCH /accounts/\{accountId\}/pages/projects/\{projectName\}. Failures per-key are reported individually — partial success is possible.


4. Feature Activation Flow (End-to-End) [Phase 1]

Admin activates "analytics" on AcmeCorp platform


NNO Portal → POST /api/features/activate


NNO Gateway → NNO Provisioning Service
  1. Validates package exists in Marketplace
  2. Validates feature not already active
  3. Creates PROVISION_ACTIVATE job


Provisioning state machine runs:
  Step 1: Create CF Worker (k3m9p2xw7q-r8n4t6y1z5-analytics-prod)
  Step 2: Create D1 database (k3m9p2xw7q-r8n4t6y1z5-analytics-db-prod)
  Step 3: Run D1 migrations
  Step 4: Set Worker secrets
  Step 5: Bind D1 to Worker

         ▼  (provisioning complete)
NNO CLI Service: POST /platforms/k3m9p2xw7q/features/activate
  Step 6: Fetch repo files via GitHub API
  Step 7: Generate updated features.config.ts
  Step 8: Update package.json + env.config.ts
  Step 9: Commit + push to GitHub

         ▼  (commit pushed)
Cloudflare Pages webhook fires (GitHub → CF Pages integration)
  Step 10: CF Pages builds new shell
  Step 11: Deploys to k3m9p2xw7q-console.pages.dev


NNO Registry: mark feature_activation as ACTIVE
NNO CLI Service: returns buildId to Provisioning
Provisioning: marks job as COMPLETED


Admin sees success in NNO Portal
End user: new feature routes appear on next login

5. Commit Strategy [Phase 1]

All commits are made under the nno-bot GitHub App identity. The commit message convention follows Conventional Commits:

ActionCommit Message Format
Platform createdchore: initialise platform \{platformId\}
Feature activatedfeat: activate \{featureId\} for \{tenantId\}
Feature deactivatedfeat: deactivate \{featureId\} for \{tenantId\}
Auth URL updatedfix: update auth service URL
Manual config syncchore: sync platform config from registry
Package version bumpchore: update \{packageName\} to \{version\}

Batching

If multiple features are activated/deactivated in the same request (bulk operation), the CLI Service batches all file changes into a single commit to avoid triggering multiple CF Pages rebuilds:

feat: activate [analytics, billing, crm] for r8n4t6y1z5

6. CF Pages Build Trigger [Phase 1]

Platform repos created from nno-stack-starter include a .github/workflows/deploy.yml that runs on push to main/develop. The workflow:

  1. Installs dependencies (pnpm install)
  2. Builds the console shell (pnpm build)
  3. Deploys to CF Pages via wrangler pages deploy ./dist --project-name=\{platformId\}-\{env\}-portal

This is the same pattern used by apps/console (the NNO operator console) and is consistent with how all CF Pages projects in this monorepo deploy. It requires CF_API_TOKEN and CF_ACCOUNT_ID to be set as neutrino-io org-level GitHub secrets — set once, inherited by every platform repo created from the template. No CF dashboard OAuth connection is required per project.

The NNO CLI Service does not call the CF Pages API directly to trigger builds in the normal activation flow — a git push to the platform repo triggers the GitHub Actions deploy workflow automatically.

Direct CF Pages API trigger (POST /platforms/\{id\}/deploy) is reserved for:

  • Manual deploys (nno deploy command)
  • Emergency redeployment without a repo change

CF Pages API Trigger

async function triggerCFPagesBuild(
  env: Env,
  projectName: string,
  branch = 'main'
): Promise<CFDeployment> {
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${env.CF_ACCOUNT_ID}/pages/projects/${projectName}/deployments`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${env.CF_API_TOKEN}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ branch }),
    }
  );
  const data = await response.json<{ result: CFDeployment }>();
  return data.result;
}

CF Pages Project Naming

EnvironmentCF Pages Project Name PatternBranch
Production\{platformId\}-consolemain
Staging\{platformId\}-consoledevelop
Preview\{platformId\}-consoleany

Note: CF Pages uses a single project per platform (\{platformId\}-console) with separate environments (production, preview) configured within the same project. There is no -\{env\} suffix in the project name.


7. GitHub File Management [Phase 1]

All file reads and writes use the GitHub Contents API:

// Read a file
async function readFile(
  octokit: Octokit,
  org: string,
  repo: string,
  path: string
): Promise<{ content: string; sha: string }> {
  const { data } = await octokit.repos.getContent({ owner: org, repo, path });
  if (Array.isArray(data)) throw new Error(`${path} is a directory`);
  return {
    content: Buffer.from(data.content, 'base64').toString('utf-8'),
    sha: data.sha,
  };
}

// Update a file (requires current SHA for optimistic locking)
async function updateFile(
  octokit: Octokit,
  org: string,
  repo: string,
  path: string,
  content: string,
  message: string,
  currentSha: string
): Promise<string> {
  const { data } = await octokit.repos.createOrUpdateFileContents({
    owner: org,
    repo,
    path,
    message,
    content: Buffer.from(content).toString('base64'),
    sha: currentSha,
  });
  return data.commit.sha;
}

Multi-File Commits (Phase 2)

GitHub's Contents API is file-by-file. For multi-file commits (e.g., updating features.config.ts + package.json + env.config.ts atomically), the NNO CLI Service uses the Git Data API (trees + blobs):

1. GET  /repos/{owner}/{repo}/git/ref/heads/main       → get current HEAD SHA
2. GET  /repos/{owner}/{repo}/git/commits/{sha}        → get tree SHA
3. POST /repos/{owner}/{repo}/git/blobs               → upload each file blob
4. POST /repos/{owner}/{repo}/git/trees               → create new tree with all blobs
5. POST /repos/{owner}/{repo}/git/commits             → create commit pointing to new tree
6. PATCH /repos/{owner}/{repo}/git/refs/heads/main    → advance HEAD to new commit

This produces a single atomic commit regardless of how many files changed.


8. Code Generation: features.config.ts [Phase 1]

The NNO CLI Service generates the contents of features.config.ts by reading the platform's active feature list from the Registry and rendering the TypeScript file programmatically.

See Shell Feature Config specification for the full schema and generation format.


9. Wrangler Configuration

# services/cli/wrangler.toml

name = "nno-k3m9p2xw7q-cli-service"
main = "src/index.ts"
compatibility_date = "2024-09-13"
compatibility_flags = ["nodejs_compat"]

# Production (default — no --env flag)
[[kv_namespaces]]
binding = "TOKENS"
id = "f2ca54f53cd84925bb7ffd5c811779a6"

[[analytics_engine_datasets]]
binding = "NNO_METRICS"
dataset = "nno_metrics"

[env.stg]
name = "nno-k3m9p2xw7q-cli-service-stg"
[[env.stg.kv_namespaces]]
binding = "TOKENS"
id = "72610903da4c4a2f9ddb80cc4e4517ae"
[[env.stg.analytics_engine_datasets]]
binding = "NNO_METRICS"
dataset = "nno_metrics"

Secrets (per environment)

SecretDescription
GITHUB_APP_IDGitHub App numeric ID
GITHUB_APP_PRIVATE_KEYPEM private key for GitHub App auth
GITHUB_APP_INSTALLATION_IDGitHub App installation ID for the NNO organisation
TEMPLATE_REPO_FULL_NAMEFull path of the template repo (e.g., neutrino-io/nno-stack-starter)
CF_API_TOKENCloudflare API token (Pages deploy scope)
CF_ACCOUNT_IDCloudflare account ID
NNO_REGISTRY_URLInternal URL of NNO Registry Worker
NNO_INTERNAL_API_KEYFor service-to-service calls to Registry
AUTH_API_KEYFor authenticating incoming CLI and portal requests
CORS_ORIGINSComma-separated allowed origins for the CLI Service
GITHUB_PATPersonal access token used as a fallback when the GitHub App token has insufficient permissions (e.g., administration:write for repo creation or access to newly-created platform repos not yet covered by the App installation scope)

10. Error Handling

ErrorHTTP StatusBehaviour
GitHub repo already exists422Skip creation, return existing repo URL
GitHub rate limit hit429Retry with Retry-After header delay
GitHub App token expired401Refresh token from KV, retry once
CF Pages project not found404Return error, provisioning rolls back
File SHA conflict (concurrent write)409Fetch new SHA, retry write once
Template repo inaccessible500Return actionable error to caller

All errors are logged to the Registry's audit_log table with action = 'CLI_SERVICE_ERROR'.


Status: Detailed design — ready for implementation Implementation target: services/cli/ Related: System Architecture §14.E · NNO Provisioning · Shell Feature Config

On this page