nn

star 2

@nextnode-solutions/nn — local dev DX CLI. Auto-load when working on nn package or any project that uses nn.

walid-mos By walid-mos schedule Updated 2/24/2026

name: nn description: "@nextnode-solutions/nn — local dev DX CLI. Auto-load when working on nn package or any project that uses nn." version: 1.2.1 user-invocable: false autoload-dirs: - /Users/walid/Development/nextnode - /Users/walid/Development/saas

nn — Local Development CLI

Published as @nextnode-solutions/nn (v1.2.1). Built with citty, uses @nextnode/cli as workspace dependency for shared types and config.

Package: packages/nn/ in the infrastructure monorepo.

Commands

Command Purpose Key Args
nn up Start local dev (Docker services + app dev server) --containerized, --reset-ports
nn down Stop local dev (kills dev server + Docker services) --clean (removes volumes)
nn env show Display resolved env vars <env> (default: local), --reveal, --diff <env>
nn env check Validate env vars against requirements <env> (default: local)
nn env init Create .env.local, .env.dev, .env.prod templates
nn env push Push env vars to GitHub environment secrets <env> (required: dev, prod), --file

nn up Flow

  1. Detect services from nextnode.toml (detectServices())
  2. Check Docker availability (only if services exist)
  3. Resolve port assignments (persisted to .nn/ports.json, reusable across restarts)
  4. Ensure .nn/ directory + .gitignore entries
  5. Read .env.localuserVars (empty if file missing)
  6. Generate missing [secrets] vars → write to .env.local (add-only, 0o600 perms)
  7. Resolve Docker env (Supabase keys + postgres password → .nn/.env)
  8. Compute auto-generated vars (buildLocalEnvVars()) — computed always wins over user
  9. Merge { ...userVars, ...computedVars }mergedVars (warns on overrides)
  10. Print env summary table with [auto]/[secret]/[user] labels
  11. Generate docker-compose.local.yml in .nn/ (if services fresh)
  12. Start Docker services + wait for healthy
  13. Detect and start app dev server (detectDevCommand())
  14. Multiplex logs (app + Docker interleaved with color-coded prefixes)

Three-Tier Env Var Sources

process.env (lowest)
  <- .env.local vars (user-provided + persisted [secrets])
    <- computed service vars from buildLocalEnvVars() + resolveDockerEnv() (highest)
  • [auto]: Service-derived vars computed by nn up (SUPABASE_URL, DATABASE_URL, REDIS_URL, etc.)
  • [secret]: Config-declared secrets generated by nn up via [secrets] in nextnode.toml
  • [user]: User-provided vars from .env.local

Supabase Key Persistence

Keys are generated/reused in resolveDockerEnv(), written to .nn/.env, then passed to buildLocalEnvVars() so both Docker and app use the same values. Single owner — no split brain.

nn env Subcommands

nn env init

  • Creates .env.local, .env.dev, .env.prod templates (skips existing files)
  • .env.local: excludes auto-generated vars (only user-provided + placeholder for secrets)
  • .env.dev / .env.prod: includes ALL vars, generates unique random [secrets] values per env
  • Ensures .gitignore includes .env.* patterns

nn env check

  • Validates required env vars for a given environment (default: local)
  • Detects required vars from: Docker Compose file, SERVICE_REQUIRED_VARS, [secrets] config
  • Local env: excludes auto-generated vars (getAutoGeneratedVars()) from required list
  • Rejects placeholder values (<...>, empty, whitespace-only) as MISSING
  • Exit code 1 if missing vars, 0 if all present

nn env push

  • Pushes env vars from .env.{env} to GitHub environment secrets
  • Blocks pushing to local (error exit)
  • Interactive checkbox picker (all selected by default)
  • Checks existing secrets, prompts to overwrite
  • Uses gh secret set --env <ghEnvName> per selected var

nn env show

  • Displays env vars with masked sensitive values by default
  • --reveal: show actual values
  • --diff <env>: compare two environments (+ only in first, - only in second, ~ different values)

Libraries (packages/nn/src/lib/)

Library Key Exports
config requireConfig() — loads nextnode.toml via @nextnode/cli, bundles nextnode.default.toml
services detectServices(config): DetectedServices, getAutoGeneratedVars(config): ReadonlySet<string> — returns max set of auto-generated var names for configured services
ports resolvePortMap(services, appPort, nnDir, opts?), isPortAvailable(port), findAvailablePort(start, max), DEFAULT_PORTS, PortMap
compose generateLocalCompose(config, services, ports), writeLocalComposeFiles(projectDir, config, composeContent) — writes compose + Supabase support files (kong.yml, roles.sql, init-db.sh, kong-entrypoint.sh) to .nn/
docker checkDockerAvailable(), startDockerServices(composePath, envFilePath?), stopDockerServices(composePath, opts?), areServicesRunning(composePath), getServiceStatuses(composePath)
env readEnvFile(env, cwd, filePath?), parseEnvFile(content), writeEnvLocal(projectDir, vars): EnvVars, buildLocalEnvVars(services, ports, supabaseKeys?, pgPassword?), isSensitive(name), maskValue(value), formatValuePreview(value, name, reveal), isPlaceholderValue(value), resolveEnvFile(env, filePath?), mapGitHubEnvName(env), SERVICE_REQUIRED_VARS, EnvVars (re-export)
oauth computeLocalOAuthEnvVars(oauthConfig, apiPort, localVars: Readonly<EnvVars>) — pure function, receives pre-parsed vars instead of reading .env.local. Falls back to process.env for credentials. Warns and skips providers with missing credentials.
secret-gen generateSecret(decl: SecretDeclaration): stringhex via randomBytes, base64url via randomBytes, uuid via randomUUID. Exhaustive default: never on discriminant.
dev-server detectDevCommand(projectDir) — checks package.json scripts.dev, then framework-specific defaults (astro, next, vite)
logs createPrefixWriter(serviceName, colorIndex), streamDockerLogs(composePath), pipeWithPrefix(proc, serviceName, colorIndex)
process gracefulKill(proc, timeoutMs), waitForHealthy(checkFn, label, maxMs), writePidFile(nnDir, pid), readPidFile(nnDir), removePidFile(nnDir), killProcess(pid), isProcessRunning(pid)

Types

type ServiceName = 'supabase' | 'redis'

interface DetectedServices {
  names: ServiceName[]
  hasSupabase: boolean
  hasRedis: boolean
}

interface PortMap {
  supabaseApi?: number    // default 54321
  supabaseStudio?: number // default 54323
  supabaseDb?: number     // default 54322
  supabaseAuth?: number   // default 9999
  redis?: number          // default 6379
  app: number             // default 4321
}

SecretDeclaration, GenerateAlgorithm, EnvVars, and ProjectConfig are defined in @nextnode/cli types — see nextnode-infra skill.

File Layout

File/Dir Purpose Written by
.env.local User secrets + generated [secrets] User + nn up (secrets only, add-only)
.nn/.env Infra vars for Docker Compose nn up (every run)
.nn/ports.json Persisted port assignments nn up
.nn/docker-compose.local.yml Local Docker Compose nn up
.nn/supabase/ Kong config, init scripts, roles.sql nn up
.nn/app.pid Dev server PID nn up
.env.dev Deploy vars for dev User + nn env init
.env.prod Deploy vars for prod User + nn env init

All secret-containing files (.env.local, .nn/.env) enforced 0o600 via chmodSync after every write.

[secrets] Config (nextnode.toml)

[secrets]
TOKEN_ENCRYPTION_KEY = { generate = "hex", bytes = 32 }
SESSION_SECRET = { generate = "base64url", bytes = 32 }
INTERNAL_API_KEY = { generate = "uuid" }
  • Three generation types: hex, base64url (both require bytes >= 16), uuid
  • nn up generates missing secrets → writes to .env.local (add-only, never overwrites)
  • Subsequent runs read persisted values from .env.local
  • Config validation lives in packages/cli/src/lib/config.ts (validateConfig())
  • Runtime generation in packages/nn/src/lib/secret-gen.ts

Auto-Generated Vars (excluded from .env.local templates and local checks)

getAutoGeneratedVars(config) returns the maximum possible set:

Service Vars
Supabase SUPABASE_URL, DATABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, JWT_SECRET, POSTGRES_PASSWORD
Redis REDIS_URL

SERVICE_REQUIRED_VARS

Service Required in deploy envs
supabase JWT_SECRET, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY, POSTGRES_PASSWORD
redis REDIS_URL
r2 R2_BUCKET_NAME, R2_ACCOUNT_ID, R2_ENDPOINT_URL

Key Patterns

  • Port persistence: .nn/ports.json reused across restarts. --reset-ports to reassign.
  • OAuth credential lookup: from localVars (pre-parsed .env.local) with process.env fallback. GitHub OAuth uses GH_* prefix (not GITHUB_*).
  • Local compose: generated from scratch (NOT transformed from deploy compose) — differences are substantial (host ports, named volumes, studio always included).
  • Sensitive masking: isSensitive() checks suffixes (_KEY, _SECRET, _PASSWORD, _TOKEN, _CREDENTIALS, _API_KEY), prefixes (JWT_), exact matches (DATABASE_URL, REDIS_URL).
  • Placeholder rejection: isPlaceholderValue() returns true for <...>, empty, whitespace-only values.
  • .nn/ cleanup: writeLocalComposeFiles() cleans up stale directory artifacts (Docker creates bind-mount paths as directories when source files don't exist).
Install via CLI
npx skills add https://github.com/walid-mos/mac-config --skill nn
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator