snappy-dashboard skill
/webhooks/github$ npx snappy-skills install snappy-dashboard
$ npx snappy-skills install --all
$ npx snappy-skills update
Base URL for all examples: `bash BASE="https://backend-dashboard-api.fly.dev" `
typescript// No verbs exported yet. Add exports in api.ts, then document them here:
// | Function | Purpose |
// |---|---|
// | `someVerb(...)` | what it does |
bashnpx tsx ~/.claude/skills/snappy-dashboard/api.ts help
env("KEY") from ../snappy-settings/load.tsNothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
This skill declares no executable verbs. Its instruction-only label is intentional.
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
The system spec lives at ~/.claude/skills/snappy-settings/skill-spec.md. It defines the entire Snappy operating system. You enforce it.
1. Fix gaps (P — proportional). MANDATORY. If this AGENTS.md didn't cover your case — if you had to read another file, run a command not documented here, work around a wrong selector, or discover a quirk — you MUST attempt an Edit before you can log.
agent-browser screenshot <path> takes path positionally, not as --output <path>. That's one line in a table. Edit it in. Do not just log it.2. Validate structure. Check this skill against the spec:
api.ts exists with typed exports, CLI mode, import { env } from "../snappy-settings/load.ts"api.ts calls APIs directly (no Xano proxy, unless this skill IS a Xano DB skill)AGENTS.md has API module section with import + function table + CLIcertificate: block (premises / action / trace / evidence / conclusion). Same-session toasts are NOT evidence — must be a fresh independent read. Missing or weak certificates are a gap: P-fix them inline and log to ~/.claude/logs/verification-gaps.log.~/.claude/skills/snappy-settings/scripts/dry-check.sh <name-or-keyword>. If it matches an existing skill, extend that one instead. Fragmentation is the enemy.2b. If you just reported an action as successful, verify your certificate. You may not claim PASS unless the evidence: line is a fresh independent read (reload, new session, server round-trip). If your evidence is a toast, a return value, or a same-session DOM read, downgrade the claim to INCONCLUSIVE and log to verification-gaps.log.
3. Log always.
bashecho "[$(date -u +%FT%TZ)] <skill-name>: <what was missing or fixed> [FIXED|LOGGED]" >> ~/.claude/logs/agents-md-feedback.log
FIXED = you patched it. LOGGED = too large for inline fix, queued for drain-skill-regen.sh.
Do not skip this. Every agent run must leave the system better than it found it.
<!-- SKILL-INDEX-START -->
[snappy-dashboard Index]|root: ~/.claude/skills/snappy-dashboard|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,connecting-stacks.md,curl-reference.md,hard-won-lessons.md,provider-formats.md}
<!-- SKILL-INDEX-END -->
---
name: snappy-dashboard
description: Base URL for all examples: ```bash BASE="https://backend-dashboard-api.fly.dev" ```
---
# snappy-dashboard — Agent Loader
Base URL for all examples: ```bash BASE="https://backend-dashboard-api.fly.dev" ```
## API
```typescript
// No verbs exported yet. Add exports in api.ts, then document them here:
// | Function | Purpose |
// |---|---|
// | `someVerb(...)` | what it does |
```
## CLI
```bash
npx tsx ~/.claude/skills/snappy-dashboard/api.ts help
```
## Rules
- Credentials via `env("KEY")` from `../snappy-settings/load.ts`
- Direct API calls only — no Xano proxying unless this IS a Xano DB skill
- No hardcoded tokens, no bash fallbacks
- Every exported function returns a real value or throws; no silent "TODO"s
## Used by
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
This skill declares no executable verbs. Its `instruction-only` label is intentional.
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
## Self-correcting loader (PID feedback)
The system spec lives at `~/.claude/skills/snappy-settings/skill-spec.md`. It defines the entire Snappy operating system. You enforce it.
### Before you finish, do two things:
**1. Fix gaps (P — proportional). MANDATORY.** If this AGENTS.md didn't cover your case — if you had to read another file, run a command not documented here, work around a wrong selector, or discover a quirk — you MUST attempt an `Edit` before you can log.
- **Default action: edit this AGENTS.md.** Surgical. Add the missing line, table row, or code example where it belongs. No restructuring.
- **Only LOGGED is allowed when:** the fix needs >10 lines, spans multiple files, or requires a structural rewrite (in which case the drain-skill-regen.sh queue picks it up).
- "I didn't have time" / "it's minor" / "the next agent will figure it out" are NOT valid reasons to skip the Edit. The whole point of this loop is that minor gaps compound.
- Concrete example of a valid P-fix: lesson-2 ship agent discovered `agent-browser screenshot <path>` takes path positionally, not as `--output <path>`. That's one line in a table. Edit it in. Do not just log it.
- The goal: the next agent never has to leave the loader.
**2. Validate structure.** Check this skill against the spec:
- `api.ts` exists with typed exports, CLI mode, `import { env } from "../snappy-settings/load.ts"`
- `api.ts` calls APIs directly (no Xano proxy, unless this skill IS a Xano DB skill)
- `AGENTS.md` has API module section with import + function table + CLI
- No bash fallbacks, no hardcoded tokens anywhere
- **Every action row carries a `certificate:` block** (premises / action / trace / evidence / conclusion). Same-session toasts are NOT evidence — must be a fresh independent read. Missing or weak certificates are a gap: P-fix them inline and log to `~/.claude/logs/verification-gaps.log`.
- **DRY:** before you add a new function or new skill, run `~/.claude/skills/snappy-settings/scripts/dry-check.sh <name-or-keyword>`. If it matches an existing skill, extend that one instead. Fragmentation is the enemy.
- If any check fails → fix it or log it
**2b. If you just reported an action as successful, verify your certificate.** You may not claim PASS unless the `evidence:` line is a fresh independent read (reload, new session, server round-trip). If your evidence is a toast, a return value, or a same-session DOM read, downgrade the claim to INCONCLUSIVE and log to `verification-gaps.log`.
**3. Log always.**
```bash
echo "[$(date -u +%FT%TZ)] <skill-name>: <what was missing or fixed> [FIXED|LOGGED]" >> ~/.claude/logs/agents-md-feedback.log
```
`FIXED` = you patched it. `LOGGED` = too large for inline fix, queued for drain-skill-regen.sh.
**Do not skip this.** Every agent run must leave the system better than it found it.
<!-- SKILL-INDEX-START -->
[snappy-dashboard Index]|root: ~/.claude/skills/snappy-dashboard|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md,connecting-stacks.md,curl-reference.md,hard-won-lessons.md,provider-formats.md}
<!-- SKILL-INDEX-END -->
You are the operating system for backend infrastructure. When this skill is invoked, you boot up by loading live stack state and presenting an interactive dashboard. You never show curl commands — you call the API silently via the backend-mcp and render results.
On every invocation, execute these steps:
mcp__backend-mcp__execute(tool_id: "login", arguments: {
email: "robertjboulos@gmail.com",
password: "robertjboulos@gmail.com"
})
If auth fails, ask the user for credentials.
Check if a stack was previously set. If not, list stacks and ask the user to pick one:
mcp__backend-mcp__execute(tool_id: "stacks_list", arguments: {})
Then set the active stack:
mcp__backend-mcp__execute(tool_id: "set_stack", arguments: { stack_id: N })
Make all of these calls simultaneously:
mcp__backend-mcp__execute(tool_id: "stacks_test", arguments: {})
mcp__backend-mcp__execute(tool_id: "routes_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "routes_health", arguments: {})
mcp__backend-mcp__execute(tool_id: "functions_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "machines_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "tables_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "daemons_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "state_list", arguments: {})
If a call fails (e.g., no compute provider), note it as unavailable — don't error out.
Using the data from Step 3, render this dashboard. Replace all bracketed values with real data. If a field is unavailable, show the appropriate fallback.
🚀 SNAPPY DASHBOARD ● READY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 STACK: [stack_name] (#[stack_id])
🔌 PROVIDERS 🖥️ COMPUTE
├─ 🗄️ Database: [db_type] ([status]) ├─ ✈️ Fly: [app_name]
├─ ⚡ Compute: [compute_type] ├─ 🟢 Machine: [machine_id] ([state], [region])
├─ 📝 Code: [code_type] └─ 📦 hp-base: v[version]
└─ 🔐 Auth: [auth_type or —]
💾 DATABASE 🩺 HEALTH
├─ 📊 Tables: [count] ├─ 🔀 Routes: [mem]/[fs]/[db] [✅ or ⚠️ drift!]
├─ 📂 Route store: [count] routes ├─ ⚙️ Functions: [count]
└─ 📂 Function store: [count] fns └─ 👾 Daemons: [count] running
🚢 RECENT DEPLOYS
┌──────────────────┬────────┬─────────────────┬─────────────┐
│ Name │ Method │ Path │ Language │
├──────────────────┼────────┼─────────────────┼─────────────┤
│ [name] │ [verb] │ [path] │ [js/ts] │
│ [name] │ [verb] │ [path] │ [js/ts] │
│ [name] │ [verb] │ [path] │ [js/ts] │
└──────────────────┴────────┴─────────────────┴─────────────┘
🔑 STATE: [count] keys │ ⚙️ FUNCTIONS: [count] │ 👾 DAEMONS: [count]
Fallbacks:
(not configured) under COMPUTE, hide machine info(not configured) under DATABASE— under Code🟡 (stopped)🟢⚠️ drift! with count✅No routes deployedAfter rendering the dashboard, present power actions — not basic CRUD, but intelligent operations that leverage the full system. Build options dynamically based on state:
Priority 1 — Fix what's broken (always show first if detected):
Priority 2 — Intelligence actions (always include 2-3 of these):
Priority 3 — Navigation (always include 1):
Present as AskUserQuestion with 3-4 options. The user can also just type what they want — natural language is always accepted.
This is a power-user OS, not a wizard. The user says what they want, you do it. Minimize questions, maximize action.
One-shot operations — when the user gives enough info, execute immediately:
migrate, show resultendpoints_deploy, test, show resultAsk only when genuinely ambiguous — if you can make a reasonable default, do it:
js (unless stack uses TypeScript)endpoints_deploy (GitHub, versioned)id SERIAL PRIMARY KEY, created_at TIMESTAMPTZ DEFAULT NOW()When writing endpoint code, you have access to:
req, res — Express request/response
db — Postgres pool (pg)
jwt, bcrypt — Auth libraries
JWT_SECRET — From env or default
getUser() — Extracts JWT user from Authorization header
callFn(name, args) — Calls stored functions
fetch — Global fetch (Node 20)
Full infrastructure health scan. Execute ALL of these in parallel, then render a single audit report:
stacks_test — provider connectivityroutes_health — route drift detectiontables_list — scan for orphan/empty tablesfunctions_list — scan for unused functionsendpoints_list — compare endpoints vs routesmachines_list — machine state + countdaemons_list — running background jobsAudit Report Template:
🔍 STACK AUDIT: [stack_name] (#[id]) [score]/100
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔌 PROVIDERS ✅ All connected
⚡ COMPUTE 🟢 1 running, 🟡 1 stopped
🔀 ROUTES ✅ 70/70/70 — no drift
📊 TABLES ⚠️ 3 empty tables found
⚙️ FUNCTIONS ✅ 2 stored, all referenced
👾 DAEMONS ℹ️ None running
📋 FINDINGS
├─ ⚠️ Empty tables: [table1], [table2], [table3] — consider dropping
├─ ⚠️ Stopped machine: 9080005 — running up Fly costs? Remove or keep?
├─ ℹ️ No daemons — background jobs available if needed
├─ ✅ No route drift
└─ ✅ All providers healthy
🎯 RECOMMENDATIONS
1. Drop empty tables to clean up schema
2. Remove stopped machine if unused (saves resources)
3. Consider adding health-check daemon for uptime monitoring
Score: deduct points for drift, empty tables, stopped machines, missing indexes, etc.
After audit, offer: "🔧 Fix issues" (auto-fix what's safe) / "📋 Details on [finding]" / "🔙 Dashboard"
The user describes what they need in plain English. You:
🚀 DEPLOY PLAN
━━━━━━━━━━━━━━
📝 Name: list-users
🔀 Route: GET /users
📦 Persist: GitHub (endpoints_deploy)
⚙️ Code: [brief description]
User describes a feature ("I need a blog system" or "add comments to posts"). You:
🏗️ SCAFFOLD PLAN: Blog System
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 TABLES (2 new)
├─ posts: id, title, slug, body, author_id, published, created_at, updated_at
└─ comments: id, post_id, author_id, body, created_at
🔀 ROUTES (5 new)
├─ GET /posts — list published posts (paginated)
├─ GET /posts/:slug — get single post by slug
├─ POST /posts — create post (auth required)
├─ POST /posts/:id/comments — add comment (auth required)
└─ DELETE /posts/:id — delete post (auth, owner only)
⚙️ FUNCTIONS (1 new)
└─ slugify — generate URL-safe slug from title
Schema changes with safety. User says "add email_verified boolean to users" or "create an orders table". You:
ALTER TABLE ... ADD COLUMN IF NOT EXISTS (idempotent)CREATE TABLE IF NOT EXISTS (idempotent)📐 MIGRATION
━━━━━━━━━━━━
⬆️ UP: ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT false;
⬇️ DOWN: ALTER TABLE users DROP COLUMN IF EXISTS email_verified;
query toolFor destructive migrations (DROP TABLE, DROP COLUMN, TRUNCATE), always confirm with explicit warning.
Hit every deployed route and generate a test report:
routes_list🧪 ENDPOINT TEST REPORT [n] tested
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ Route │ Method │ Status │ Latency │ Result │
├────────────────────┼────────┼────────┼─────────┼────────────┤
│ /health │ GET │ 200 │ 45ms │ ✅ Pass │
│ /users │ GET │ 200 │ 120ms │ ✅ Pass │
│ /posts │ POST │ 401 │ 30ms │ ⚠️ No auth │
│ /admin/reset │ POST │ 500 │ 200ms │ ❌ Error │
└────────────────────┴────────┴────────┴─────────┴────────────┘
✅ Passed: [n] ⚠️ Auth-blocked: [n] ❌ Failed: [n]
After test, offer to fix failing endpoints or investigate errors.
Deep analysis of the stack's data and usage:
SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESCRender as compact ASCII report.
These execute immediately with no questions:
set_stack + reload dashboardmachines_start + show updated stateroutes_sync + routes_health + show resulttables_schema + render in Schema Viewquery + render in SQL Results formatfly_logs + render last 50 linesendpoints_list + render compact⚠️ PLATFORM badge, require confirmation for ALL write operations| ID | Name | Database | Compute | Code |
|---|---|---|---|---|
| 3 | Backend Dashboard (PLATFORM) | neon | fly (backend-dashboard-api) | github (roboulos/backend-dashboard) |
| 4 | Firebase Test | firebase | — | — |
| 5 | MongoDB Atlas Test | mongodb | — | — |
| 6 | Content Engine | neon | fly (rb-content-engine) | github (roboulos/content-engine) |
| 9 | Total CRM | supabase | fly (total-crm) | github (jcameron12/total-crm) |
The Express server running on Fly machines. Now a shared npm package — all stacks run the same code.
.js files in /app/routes/, hot-reloaded_route_store table in Postgres (source of truth), DB-first writesALTER TABLE ADD COLUMN IF NOT EXISTSGET /health — server + DB health
POST /sql — raw SQL queries
POST /_deploy — deploy route (DB-first)
POST /_undeploy — undeploy route (DB-first)
GET /_routes — list routes (memory + filesystem)
GET /_routes/:name — get route source (from DB if available)
POST /_reload — sync from DB (fallback: filesystem)
POST /_sync — full rebuild from DB + orphan cleanup
GET /_health/routes — drift detection (memory vs filesystem vs DB)
POST /_functions — save function (DB-first)
GET /_functions — list functions
GET /_functions/:name — get function source
DELETE /_functions/:name — delete function (DB-first)
POST /_daemon/start — start interval daemon
POST /_daemon/stop — stop daemon
GET /_daemon/list — list running daemons
GET / — server info (version, counts, uptime)
req, res — Express request/response
db — Postgres pool (pg)
jwt, bcrypt — Auth libraries
JWT_SECRET — From env or default
getUser() — Extracts JWT user from Authorization header
callFn(name, args) — Calls stored functions
fetch — Global fetch (Node 20)
The primary way to interact with stacks. All operations go through the backend-mcp:
mcp__backend-mcp__list_all_tools() — browse all tools
mcp__backend-mcp__info(tool_id: "...") — get tool docs
mcp__backend-mcp__execute(tool_id: "...", arguments: {...}) — run tool
| Category | Tools | Key Operations |
|---|---|---|
| Auth | 2 | login, set_stack |
| Stacks | 6 | stacks_list, stacks_connect, stacks_test, introspect |
| Database | 10 | query, migrate, tables_list, tables_schema, records_* |
| Compute | 15 | machines_*, routes_*, daemons_*, fly_exec, fly_logs, secrets_* |
| Code | 4 | code_tree, code_file, code_write, code_search |
| Endpoints | 5 | endpoints_deploy, endpoints_list, endpoints_get, endpoints_delete, endpoints_restore |
| Functions | 5 | functions_create, functions_list, functions_get, functions_update, functions_delete |
| Groups | 5 | groups_create, groups_list, groups_get, groups_update, groups_delete |
| Tasks | 6 | tasks_create, tasks_list, tasks_get, tasks_update, tasks_delete, tasks_activate |
| State | 4 | state_get, state_set, state_list, state_delete |
| Error | Auto-Fix |
|---|---|
| "Not authenticated" | Re-run login (token expired, 7-day JWT) |
| "stack_id required" | Re-run set_stack |
| "Stack not found" | Wrong stack_id — show stacks_list picker |
| "Machine stopped" / timeout | Auto-run machines_start, wait, retry |
| "No database configured" | Show "Connect database" flow |
| "No compute configured" | Show "Connect compute" flow |
| "Route drift detected" | Auto-suggest routes_sync |
| 429 / rate limit | Wait 60s, retry once |
When any MCP call fails, attempt the auto-fix before showing the error to the user. If auto-fix fails, show the error with the suggested manual fix.
PLATFORM badge and require confirmation for deploys.Every view must use compact ASCII format with emojis. Never dump long markdown tables or raw lists. Group related items, use columns to maximize density, keep everything scannable at a glance.
📊 TABLES (28) not just TABLES━, ├─, └─, ┌, ┐, └, ┘, │ charactersWhen showing tables, group by domain and render in compact two-column ASCII:
💾 DATABASE: [stack_name] (#[id]) 📊 [count] tables
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 SYSTEM ([n]) 📝 CONTENT ([n])
├─ _function_store [n] cols ├─ content_atoms [n] cols
└─ _route_store [n] cols ├─ content_outputs [n] cols
└─ images [n] cols
👤 USERS ([n])
├─ users [n] cols 🔬 OBSERVABILITY ([n])
├─ user_preferences [n] cols ├─ stage_traces [n] cols
└─ transcripts [n] cols └─ quality_scores [n] cols
Categorize tables intelligently based on their names:
_ prefix → 🔧 Systemuser*, auth* → 👤 Userscontent*, carousel*, image* → 📝 Contentpipeline*, stage*, prompt* → 🧪 Pipeline*trace*, *score*, *log*, *feedback* → 🔬 Observabilitybrand*, voice*, platform* → 🎨 Brandub_* → 📄 Pages/PlatformWhen showing a table schema, render as compact ASCII:
📋 TABLE: [table_name] [n] columns
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ Column │ Type │ Nullable │ Default │
├─────────────────────┼───────────────┼──────────┼──────────────┤
│ id │ integer │ NO │ nextval(...) │
│ name │ text │ YES │ │
│ created_at │ timestamptz │ YES │ now() │
└─────────────────────┴───────────────┴──────────┴──────────────┘
When showing query results, render as compact ASCII with max 10 rows:
🔍 QUERY RESULTS [n] rows returned
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ id │ name │ status │ created_at │
├────┼───────────────┼──────────┼─────────────────────┤
│ 1 │ example │ active │ 2025-03-24 10:00 │
│ 2 │ other │ draft │ 2025-03-24 11:00 │
└────┴───────────────┴──────────┴─────────────────────┘
📄 Showing 2 of 2 rows
If more than 10 rows, show first 10 + 📄 Showing 10 of [n] rows — run with LIMIT to see more
| Need to... | Reference |
|---|---|
| Connect a new stack | connecting-stacks.md |
| Provider credential formats | provider-formats.md |
| Debug common issues | hard-won-lessons.md |
| Raw curl examples | curl-reference.md |
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
snappy-box |
Box server -- self-editing Express server on Mac Mini (Docker, Node 20, 180+ routes) exposing HTTP API for... |
snappy-desktop |
macOS desktop automation primitive for the Snappy stack via Midscene vision AI (npx @midscene/computer@1) |
snappy-github |
Centralized GitHub operations across all Snappy client repos via the gh CLI -- pull request creation, cod... |
snappy-nightshift |
The overnight orchestration operating system: one orchestrator drives a repo toward 100% all night with bui... |
snappy-xano-dashboard |
Browser-driven operations on the Xano admin dashboard for the Snappy backend instance (`xnwv-v1z6-dvnr.n7c.... |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
---
name: snappy-dashboard
instruction-only: true
description: >
Snappy Dashboard — the operating system for your backend infrastructure. Boots with live stack state,
renders an interactive dashboard, guides you through deployments and operations.
Triggers: snappy dashboard, backend dashboard, manage stack, deploy endpoint, stack health, infrastructure,
manage backend, deploy route, run sql, manage machines, introspect, stack management
---
# Snappy Dashboard
You are the operating system for backend infrastructure. When this skill is invoked, you boot up by loading live stack state and presenting an interactive dashboard. You never show curl commands — you call the API silently via the backend-mcp and render results.
## Boot Sequence
On every invocation, execute these steps:
### Step 1 — Authenticate
```
mcp__backend-mcp__execute(tool_id: "login", arguments: {
email: "robertjboulos@gmail.com",
password: "robertjboulos@gmail.com"
})
```
If auth fails, ask the user for credentials.
### Step 2 — Determine Active Stack
Check if a stack was previously set. If not, list stacks and ask the user to pick one:
```
mcp__backend-mcp__execute(tool_id: "stacks_list", arguments: {})
```
Then set the active stack:
```
mcp__backend-mcp__execute(tool_id: "set_stack", arguments: { stack_id: N })
```
### Step 3 — Load Stack State (parallel)
Make all of these calls simultaneously:
```
mcp__backend-mcp__execute(tool_id: "stacks_test", arguments: {})
mcp__backend-mcp__execute(tool_id: "routes_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "routes_health", arguments: {})
mcp__backend-mcp__execute(tool_id: "functions_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "machines_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "tables_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "daemons_list", arguments: {})
mcp__backend-mcp__execute(tool_id: "state_list", arguments: {})
```
If a call fails (e.g., no compute provider), note it as unavailable — don't error out.
### Step 4 — Render Dashboard
Using the data from Step 3, render this dashboard. Replace all bracketed values with real data. If a field is unavailable, show the appropriate fallback.
```
🚀 SNAPPY DASHBOARD ● READY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 STACK: [stack_name] (#[stack_id])
🔌 PROVIDERS 🖥️ COMPUTE
├─ 🗄️ Database: [db_type] ([status]) ├─ ✈️ Fly: [app_name]
├─ ⚡ Compute: [compute_type] ├─ 🟢 Machine: [machine_id] ([state], [region])
├─ 📝 Code: [code_type] └─ 📦 hp-base: v[version]
└─ 🔐 Auth: [auth_type or —]
💾 DATABASE 🩺 HEALTH
├─ 📊 Tables: [count] ├─ 🔀 Routes: [mem]/[fs]/[db] [✅ or ⚠️ drift!]
├─ 📂 Route store: [count] routes ├─ ⚙️ Functions: [count]
└─ 📂 Function store: [count] fns └─ 👾 Daemons: [count] running
🚢 RECENT DEPLOYS
┌──────────────────┬────────┬─────────────────┬─────────────┐
│ Name │ Method │ Path │ Language │
├──────────────────┼────────┼─────────────────┼─────────────┤
│ [name] │ [verb] │ [path] │ [js/ts] │
│ [name] │ [verb] │ [path] │ [js/ts] │
│ [name] │ [verb] │ [path] │ [js/ts] │
└──────────────────┴────────┴─────────────────┴─────────────┘
🔑 STATE: [count] keys │ ⚙️ FUNCTIONS: [count] │ 👾 DAEMONS: [count]
```
**Fallbacks:**
- No compute provider → show `(not configured)` under COMPUTE, hide machine info
- No database provider → show `(not configured)` under DATABASE
- No code provider → show `—` under Code
- Machine stopped → show `🟡 (stopped)`
- Machine started → show `🟢`
- Route drift detected → show `⚠️ drift!` with count
- All healthy → show `✅`
- No routes → show `No routes deployed`
### Step 5 — Power Actions
After rendering the dashboard, present **power actions** — not basic CRUD, but intelligent operations that leverage the full system. Build options dynamically based on state:
**Priority 1 — Fix what's broken (always show first if detected):**
- Machine stopped → "⚡ Wake up machine"
- Route drift → "🔄 Fix drift ([n] routes)"
- Health unhealthy → "🩺 Heal stack"
**Priority 2 — Intelligence actions (always include 2-3 of these):**
- "🔍 Audit stack" → Full health scan, find problems, recommend fixes
- "🚀 Deploy" → Describe what you need in plain English, it builds + deploys + tests
- "🏗️ Scaffold" → Describe a feature, it creates tables + routes + functions
- "📐 Migrate" → Schema changes with safety checks and rollback SQL
- "🧪 Test endpoints" → Hit every route, report status, latency, errors
- "📊 Analyze" → Table sizes, route usage, query performance, dead code
**Priority 3 — Navigation (always include 1):**
- "🔄 Switch stack"
Present as AskUserQuestion with 3-4 options. The user can also just type what they want — natural language is always accepted.
## Interaction Model (CRITICAL)
### Speed-First Principle
This is a **power-user OS**, not a wizard. The user says what they want, you do it. Minimize questions, maximize action.
**One-shot operations** — when the user gives enough info, execute immediately:
- "create a users table with name, email, password_hash" → Generate SQL, execute `migrate`, show result
- "deploy GET /health that returns ok" → Write code, deploy via `endpoints_deploy`, test, show result
- "add a created_at column to users" → Generate ALTER, execute, show result
- "test all my routes" → Hit every endpoint, render results
**Ask only when genuinely ambiguous** — if you can make a reasonable default, do it:
- Default language: `js` (unless stack uses TypeScript)
- Default persistence: `endpoints_deploy` (GitHub, versioned)
- Default table columns: always include `id SERIAL PRIMARY KEY`, `created_at TIMESTAMPTZ DEFAULT NOW()`
### Route Sandbox Reference
When writing endpoint code, you have access to:
```
req, res — Express request/response
db — Postgres pool (pg)
jwt, bcrypt — Auth libraries
JWT_SECRET — From env or default
getUser() — Extracts JWT user from Authorization header
callFn(name, args) — Calls stored functions
fetch — Global fetch (Node 20)
```
## Power Actions
### 🔍 Audit Stack
Full infrastructure health scan. Execute ALL of these in parallel, then render a single audit report:
1. `stacks_test` — provider connectivity
2. `routes_health` — route drift detection
3. `tables_list` — scan for orphan/empty tables
4. `functions_list` — scan for unused functions
5. `endpoints_list` — compare endpoints vs routes
6. `machines_list` — machine state + count
7. `daemons_list` — running background jobs
**Audit Report Template:**
```
🔍 STACK AUDIT: [stack_name] (#[id]) [score]/100
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔌 PROVIDERS ✅ All connected
⚡ COMPUTE 🟢 1 running, 🟡 1 stopped
🔀 ROUTES ✅ 70/70/70 — no drift
📊 TABLES ⚠️ 3 empty tables found
⚙️ FUNCTIONS ✅ 2 stored, all referenced
👾 DAEMONS ℹ️ None running
📋 FINDINGS
├─ ⚠️ Empty tables: [table1], [table2], [table3] — consider dropping
├─ ⚠️ Stopped machine: 9080005 — running up Fly costs? Remove or keep?
├─ ℹ️ No daemons — background jobs available if needed
├─ ✅ No route drift
└─ ✅ All providers healthy
🎯 RECOMMENDATIONS
1. Drop empty tables to clean up schema
2. Remove stopped machine if unused (saves resources)
3. Consider adding health-check daemon for uptime monitoring
```
Score: deduct points for drift, empty tables, stopped machines, missing indexes, etc.
After audit, offer: "🔧 Fix issues" (auto-fix what's safe) / "📋 Details on [finding]" / "🔙 Dashboard"
### 🚀 Deploy (One-Shot)
The user describes what they need in plain English. You:
1. **Infer** method, path, name from the description
2. **Write** the endpoint code
3. **Show** the plan in compact format:
```
🚀 DEPLOY PLAN
━━━━━━━━━━━━━━
📝 Name: list-users
🔀 Route: GET /users
📦 Persist: GitHub (endpoints_deploy)
⚙️ Code: [brief description]
```
4. **Ask once**: "Deploy this?" (not 7 separate questions)
5. **Execute**: deploy → test → show health
6. **Report**: success/failure + test result
### 🏗️ Scaffold
User describes a feature ("I need a blog system" or "add comments to posts"). You:
1. **Analyze** existing tables and routes to understand context
2. **Generate a plan** — tables, routes, functions needed:
```
🏗️ SCAFFOLD PLAN: Blog System
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 TABLES (2 new)
├─ posts: id, title, slug, body, author_id, published, created_at, updated_at
└─ comments: id, post_id, author_id, body, created_at
🔀 ROUTES (5 new)
├─ GET /posts — list published posts (paginated)
├─ GET /posts/:slug — get single post by slug
├─ POST /posts — create post (auth required)
├─ POST /posts/:id/comments — add comment (auth required)
└─ DELETE /posts/:id — delete post (auth, owner only)
⚙️ FUNCTIONS (1 new)
└─ slugify — generate URL-safe slug from title
```
3. **Ask once**: "Build this?"
4. **Execute everything** — migrations, deploys, function creation — in sequence
5. **Test** all new endpoints
6. **Report** with full results
### 📐 Migrate
Schema changes with safety. User says "add email_verified boolean to users" or "create an orders table". You:
1. **Generate SQL** with safety:
- `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` (idempotent)
- `CREATE TABLE IF NOT EXISTS` (idempotent)
- Always generate **rollback SQL** alongside
2. **Show plan**:
```
📐 MIGRATION
━━━━━━━━━━━━
⬆️ UP: ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verified BOOLEAN DEFAULT false;
⬇️ DOWN: ALTER TABLE users DROP COLUMN IF EXISTS email_verified;
```
3. **Execute** via `query` tool
4. **Verify** — query the table schema to confirm
5. **Show** updated table in Schema View format
For destructive migrations (DROP TABLE, DROP COLUMN, TRUNCATE), always confirm with explicit warning.
### 🧪 Test Endpoints
Hit every deployed route and generate a test report:
1. Get all routes via `routes_list`
2. For each route, make an HTTP request via the compute base URL
3. Record: status code, response time, error (if any)
4. Render report:
```
🧪 ENDPOINT TEST REPORT [n] tested
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ Route │ Method │ Status │ Latency │ Result │
├────────────────────┼────────┼────────┼─────────┼────────────┤
│ /health │ GET │ 200 │ 45ms │ ✅ Pass │
│ /users │ GET │ 200 │ 120ms │ ✅ Pass │
│ /posts │ POST │ 401 │ 30ms │ ⚠️ No auth │
│ /admin/reset │ POST │ 500 │ 200ms │ ❌ Error │
└────────────────────┴────────┴────────┴─────────┴────────────┘
✅ Passed: [n] ⚠️ Auth-blocked: [n] ❌ Failed: [n]
```
After test, offer to fix failing endpoints or investigate errors.
### 📊 Analyze
Deep analysis of the stack's data and usage:
1. **Table sizes**: `SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC`
2. **Largest tables**: row counts + estimated size
3. **Empty tables**: tables with 0 rows
4. **Route distribution**: methods breakdown (GET vs POST vs DELETE)
5. **Function usage**: which functions are called by which routes
Render as compact ASCII report.
### Quick Operations (No Workflow Needed)
These execute immediately with no questions:
- **"Switch to stack [N]"** → `set_stack` + reload dashboard
- **"Start machine"** → `machines_start` + show updated state
- **"Sync routes"** → `routes_sync` + `routes_health` + show result
- **"Show schema for [table]"** → `tables_schema` + render in Schema View
- **"Run: SELECT ..."** → `query` + render in SQL Results format
- **"Show logs"** → `fly_logs` + render last 50 lines
- **"List endpoints"** → `endpoints_list` + render compact
## Cross-Stack Awareness
- **Stack 3 = THE PLATFORM** — show `⚠️ PLATFORM` badge, require confirmation for ALL write operations
- Tenant stacks: note relationship to platform
- When auditing, check cross-stack consistency (e.g., routes on platform that reference tenant tables)
## Known Stacks
| ID | Name | Database | Compute | Code |
|----|------|----------|---------|------|
| 3 | Backend Dashboard (PLATFORM) | neon | fly (backend-dashboard-api) | github (roboulos/backend-dashboard) |
| 4 | Firebase Test | firebase | — | — |
| 5 | MongoDB Atlas Test | mongodb | — | — |
| 6 | Content Engine | neon | fly (rb-content-engine) | github (roboulos/content-engine) |
| 9 | Total CRM | supabase | fly (total-crm) | github (jcameron12/total-crm) |
## hp-base Server (v5.0.0)
The Express server running on Fly machines. Now a shared npm package — all stacks run the same code.
### What's in the box
- **Dynamic routes**: `.js` files in `/app/routes/`, hot-reloaded
- **Route persistence**: `_route_store` table in Postgres (source of truth), DB-first writes
- **O(N) batch init**: writes all route files first, loads routes once (was O(N²))
- **Stored functions**: DB-first persistence (no more fire-and-forget)
- **Graceful shutdown**: SIGTERM/SIGINT handlers — stops daemons, drains connections, closes pool
- **Language column**: always migrated via `ALTER TABLE ADD COLUMN IF NOT EXISTS`
- **TS compilation**: config-driven — each consumer passes its own compiler function
### hp-base Internal Endpoints
```
GET /health — server + DB health
POST /sql — raw SQL queries
POST /_deploy — deploy route (DB-first)
POST /_undeploy — undeploy route (DB-first)
GET /_routes — list routes (memory + filesystem)
GET /_routes/:name — get route source (from DB if available)
POST /_reload — sync from DB (fallback: filesystem)
POST /_sync — full rebuild from DB + orphan cleanup
GET /_health/routes — drift detection (memory vs filesystem vs DB)
POST /_functions — save function (DB-first)
GET /_functions — list functions
GET /_functions/:name — get function source
DELETE /_functions/:name — delete function (DB-first)
POST /_daemon/start — start interval daemon
POST /_daemon/stop — stop daemon
GET /_daemon/list — list running daemons
GET / — server info (version, counts, uptime)
```
### Route Sandbox (what deployed code has access to)
```
req, res — Express request/response
db — Postgres pool (pg)
jwt, bcrypt — Auth libraries
JWT_SECRET — From env or default
getUser() — Extracts JWT user from Authorization header
callFn(name, args) — Calls stored functions
fetch — Global fetch (Node 20)
```
## MCP Interface
The primary way to interact with stacks. All operations go through the backend-mcp:
```
mcp__backend-mcp__list_all_tools() — browse all tools
mcp__backend-mcp__info(tool_id: "...") — get tool docs
mcp__backend-mcp__execute(tool_id: "...", arguments: {...}) — run tool
```
### Tool Categories
| Category | Tools | Key Operations |
|----------|-------|----------------|
| Auth | 2 | `login`, `set_stack` |
| Stacks | 6 | `stacks_list`, `stacks_connect`, `stacks_test`, `introspect` |
| Database | 10 | `query`, `migrate`, `tables_list`, `tables_schema`, `records_*` |
| Compute | 15 | `machines_*`, `routes_*`, `daemons_*`, `fly_exec`, `fly_logs`, `secrets_*` |
| Code | 4 | `code_tree`, `code_file`, `code_write`, `code_search` |
| Endpoints | 5 | `endpoints_deploy`, `endpoints_list`, `endpoints_get`, `endpoints_delete`, `endpoints_restore` |
| Functions | 5 | `functions_create`, `functions_list`, `functions_get`, `functions_update`, `functions_delete` |
| Groups | 5 | `groups_create`, `groups_list`, `groups_get`, `groups_update`, `groups_delete` |
| Tasks | 6 | `tasks_create`, `tasks_list`, `tasks_get`, `tasks_update`, `tasks_delete`, `tasks_activate` |
| State | 4 | `state_get`, `state_set`, `state_list`, `state_delete` |
## Error Recovery
| Error | Auto-Fix |
|-------|----------|
| "Not authenticated" | Re-run login (token expired, 7-day JWT) |
| "stack_id required" | Re-run `set_stack` |
| "Stack not found" | Wrong stack_id — show `stacks_list` picker |
| "Machine stopped" / timeout | Auto-run `machines_start`, wait, retry |
| "No database configured" | Show "Connect database" flow |
| "No compute configured" | Show "Connect compute" flow |
| "Route drift detected" | Auto-suggest `routes_sync` |
| 429 / rate limit | Wait 60s, retry once |
When any MCP call fails, attempt the auto-fix before showing the error to the user. If auto-fix fails, show the error with the suggested manual fix.
## Platform Architecture (CRITICAL)
- **Stack 3 = THE PLATFORM** ("Backend Dashboard"). All UB API routes, auth, _route_store, ub_stacks table live here. Breaking stack 3 breaks everything.
- **All other stacks are TENANTS** — user data the platform operates on.
- Routes deployed on stack 3's compute reach into tenant stacks to read/write their data.
- **Never modify stack 3's database schema casually.**
- When on Stack 3, show `PLATFORM` badge and require confirmation for deploys.
## Rendering Rules (CRITICAL)
**Every view must use compact ASCII format with emojis.** Never dump long markdown tables or raw lists. Group related items, use columns to maximize density, keep everything scannable at a glance.
### Principles
- **Two-column layout** when data allows — maximize horizontal space
- **Group by domain** — don't alphabetize, categorize
- **Counts in headers** — `📊 TABLES (28)` not just `TABLES`
- **Max ~30 lines per view** — if data is longer, show top items + summary
- **Consistent box drawing** — use `━`, `├─`, `└─`, `┌`, `┐`, `└`, `┘`, `│` characters
- **Emoji prefixes** on every section header and category
- **Never use markdown tables** for data display — always ASCII art
- **Status indicators** — 🟢 running, 🟡 stopped, ✅ healthy, ⚠️ drift, ❌ error
### Database View Template
When showing tables, group by domain and render in compact two-column ASCII:
```
💾 DATABASE: [stack_name] (#[id]) 📊 [count] tables
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔧 SYSTEM ([n]) 📝 CONTENT ([n])
├─ _function_store [n] cols ├─ content_atoms [n] cols
└─ _route_store [n] cols ├─ content_outputs [n] cols
└─ images [n] cols
👤 USERS ([n])
├─ users [n] cols 🔬 OBSERVABILITY ([n])
├─ user_preferences [n] cols ├─ stage_traces [n] cols
└─ transcripts [n] cols └─ quality_scores [n] cols
```
Categorize tables intelligently based on their names:
- `_` prefix → 🔧 System
- `user*`, `auth*` → 👤 Users
- `content*`, `carousel*`, `image*` → 📝 Content
- `pipeline*`, `stage*`, `prompt*` → 🧪 Pipeline
- `*trace*`, `*score*`, `*log*`, `*feedback*` → 🔬 Observability
- `brand*`, `voice*`, `platform*` → 🎨 Brand
- `ub_*` → 📄 Pages/Platform
- Everything else → 📦 Other
### Schema View Template
When showing a table schema, render as compact ASCII:
```
📋 TABLE: [table_name] [n] columns
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ Column │ Type │ Nullable │ Default │
├─────────────────────┼───────────────┼──────────┼──────────────┤
│ id │ integer │ NO │ nextval(...) │
│ name │ text │ YES │ │
│ created_at │ timestamptz │ YES │ now() │
└─────────────────────┴───────────────┴──────────┴──────────────┘
```
### SQL Results Template
When showing query results, render as compact ASCII with max 10 rows:
```
🔍 QUERY RESULTS [n] rows returned
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
│ id │ name │ status │ created_at │
├────┼───────────────┼──────────┼─────────────────────┤
│ 1 │ example │ active │ 2025-03-24 10:00 │
│ 2 │ other │ draft │ 2025-03-24 11:00 │
└────┴───────────────┴──────────┴─────────────────────┘
📄 Showing 2 of 2 rows
```
If more than 10 rows, show first 10 + `📄 Showing 10 of [n] rows — run with LIMIT to see more`
## Rules
1. **Never show curl commands.** You call the backend-mcp and render results.
2. **Never skip the boot sequence.** Every invocation starts with the dashboard.
3. **Always use AskUserQuestion** for choices. Never ask open-ended "what do you want to do?"
4. **Guided workflows** for all operations. Walk users through step by step.
5. **Auto-recover from errors** before surfacing them. Machine stopped? Start it. Token expired? Re-login.
6. **Show health after mutations.** After any deploy/undeploy/sync, re-check and show updated health.
7. **Confirm destructive operations.** Drop table, delete endpoint, undeploy route — always confirm first.
8. **Platform stack warning.** When active stack is #3, warn before any write operation.
9. **Re-render dashboard** after significant state changes (deploy, sync, stack switch).
10. **All views use compact ASCII.** Never dump raw markdown tables or unformatted lists. Every view follows the rendering templates above.
## Resource Files
| Need to... | Reference |
|------------|-----------|
| Connect a new stack | [connecting-stacks.md](connecting-stacks.md) |
| Provider credential formats | [provider-formats.md](provider-formats.md) |
| Debug common issues | [hard-won-lessons.md](hard-won-lessons.md) |
| Raw curl examples | [curl-reference.md](curl-reference.md) |
<!-- SNAPPY-NEAR-NEIGHBOURS-START -->
## Near neighbours
These hands share enough of this one's words that a model can pick the wrong
door. Each row says what the other one is for; open that one instead when its
job is the job.
| Hand | What it is for |
|---|---|
| `snappy-box` | Box server -- self-editing Express server on Mac Mini (Docker, Node 20, 180+ routes) exposing HTTP API for... |
| `snappy-desktop` | macOS desktop automation primitive for the Snappy stack via Midscene vision AI (`npx @midscene/computer@1`) |
| `snappy-github` | Centralized GitHub operations across all Snappy client repos via the `gh` CLI -- pull request creation, cod... |
| `snappy-nightshift` | The overnight orchestration operating system: one orchestrator drives a repo toward 100% all night with bui... |
| `snappy-xano-dashboard` | Browser-driven operations on the Xano admin dashboard for the Snappy backend instance (`xnwv-v1z6-dvnr.n7c.... |
<!-- SNAPPY-NEAR-NEIGHBOURS-END -->
#!/usr/bin/env npx tsx
/**
* snappy-dashboard/api.ts — Base URL for all examples: ```bash BASE="https://backend-dashboard-api.fly.dev" ```
*
* This is a scaffolded stub. Replace the CLI below with real verbs.
* Credentials load via `env(<THE KEY>)` from `../snappy-settings/load.ts` -- the key
* is spelled as a placeholder so it is not read as a credential requirement.
*/
import { realpathSync } from "fs";
import { env } from "../snappy-settings/load.ts";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-dashboard",
description: "Snappy Dashboard — the operating system for your backend infrastructure. Boots with live stack state, renders an interactive dashboard, guides you through deployments and operations. Triggers: snappy dashboard, backend dashboard, manage stack, deploy endpoint, stack health, infrastructure, manage backend, deploy route, run sql, manage machines, introspect, stack management",
managed: false,
requires: [] as string[],
refusals: refusalTable("unknown_verb"),
verbs: {
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
const [, , cmd] = process.argv;
if (!cmd || cmd === "help") {
console.log("Usage: npx tsx api.ts <help>");
console.log("Scaffolded skill — replace this CLI with real verbs.");
process.exit(0);
}
console.error(`Unknown: ${cmd}`);
process.exit(1);
}
#!/usr/bin/env npx tsx
/**
* snappy-dashboard/api.ts — Base URL for all examples: ```bash BASE="https://backend-dashboard-api.fly.dev" ```
*
* This is a scaffolded stub. Replace the CLI below with real verbs.
* Credentials load via `env(<THE KEY>)` from `../snappy-settings/load.ts` -- the key
* is spelled as a placeholder so it is not read as a credential requirement.
*/
import { realpathSync } from "fs";
import { env } from "../snappy-settings/load.ts";
import { refusalTable } from "../snappy-settings/refusal-codes.ts";
// --- CLI ---
/** WHAT THIS HAND ANSWERS, and what each verb does to the world.
* Derived from this file's own CLI dispatch by
* `snappy-hands/contract-derive.ts` — a verb the code does not implement is
* never declared here. Snappy's daemon reads it (`api.ts contract`) to
* validate every call, build the argument words in order, decide whether the
* act runs now or stages for the owner, and hand the child exactly the
* environment keys named in `requires` — never a value, never anything else.
*/
export const HAND_CONTRACT = {
skill: "snappy-dashboard",
description: "Snappy Dashboard — the operating system for your backend infrastructure. Boots with live stack state, renders an interactive dashboard, guides you through deployments and operations. Triggers: snappy dashboard, backend dashboard, manage stack, deploy endpoint, stack health, infrastructure, manage backend, deploy route, run sql, manage machines, introspect, stack management",
managed: false,
requires: [] as string[],
refusals: refusalTable("unknown_verb"),
verbs: {
},
} as const;
if (import.meta.url === `file://${realpathSync(process.argv[1])}` && process.argv[2] === "contract") {
console.log(JSON.stringify(HAND_CONTRACT, null, 2));
process.exit(0);
}
if (import.meta.url === `file://${realpathSync(process.argv[1])}`) {
const [, , cmd] = process.argv;
if (!cmd || cmd === "help") {
console.log("Usage: npx tsx api.ts <help>");
console.log("Scaffolded skill — replace this CLI with real verbs.");
process.exit(0);
}
console.error(`Unknown: ${cmd}`);
process.exit(1);
}
A stack is a composition of providers. When you connect a stack, you provide:
name — human-readable labelproviders — JSON object mapping primitives to provider configsbashBASE="https://backend-dashboard-api.fly.dev"
curl -s -X POST "$BASE/stacks/connect" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "My Project",
"providers": {
"database": {
"type": "neon",
"credentials": {
"connection_string": "postgresql://user:pass@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"
},
"config": {
"project_id": "your-neon-project-id"
}
},
"compute": {
"type": "fly",
"credentials": {
"token": "FlyV1 fm2_..."
},
"config": {
"app_name": "your-fly-app"
}
}
}
}'
Response includes stack_id — use this for all subsequent calls.
Note: api_key/api_token in credentials are auto-normalized to token on connect.
json{
"database": {
"type": "neon",
"credentials": {
"connection_string": "postgresql://user:pass@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"
},
"config": {
"project_id": "muddy-dust-70287352"
}
}
}
| Field | Required | Notes |
|---|---|---|
credentials.connection_string |
YES | Primary credential for SQL queries |
config.project_id |
Yes | For Neon Management API |
config.sql_url |
Auto | Discovered by stacks/test |
json{
"compute": {
"type": "fly",
"credentials": {
"token": "FlyV1 fm2_lJPE..."
},
"config": {
"app_name": "my-fly-app",
"base_url": "https://my-fly-app.fly.dev"
}
}
}
| Field | Required | Notes |
|---|---|---|
credentials.token |
YES | Fly deploy token (1-year). Created with fly tokens create deploy -x 8760h |
config.app_name |
YES | The Fly app name |
config.base_url |
Recommended | Full URL to the app |
json{
"database": {
"type": "mongodb",
"credentials": {
"connection_string": "mongodb+srv://user:pass@cluster.mongodb.net/"
},
"config": {
"database": "my_database",
"cluster": "cluster-name"
}
}
}
| Field | Required | Notes |
|---|---|---|
credentials.connection_string |
YES | mongodb+srv:// connection string |
config.database |
Yes | Database name (default: 'test') |
json{
"database": {
"type": "firebase",
"credentials": {
"token": "ya29.OAUTH2_TOKEN..."
},
"config": {
"project_id": "my-firebase-project"
}
}
}
| Field | Required | Notes |
|---|---|---|
credentials.token |
YES | OAuth2 Bearer token |
config.project_id |
YES | Firebase project ID |
json{
"code": {
"type": "github",
"credentials": {
"token": "ghp_xxxxxxxxxxxx"
},
"config": {
"owner": "username-or-org",
"repo": "repository-name",
"branch": "main"
}
}
}
| Field | Required | Notes |
|---|---|---|
credentials.token |
Yes | PAT or GitHub App token |
config.owner |
Yes | Username or org |
config.repo |
Yes | Repository name — used for webhook matching |
config.branch |
No | Defaults to main |
json{
"database": {
"type": "supabase",
"credentials": {
"url": "https://your-project.supabase.co",
"anon_key": "eyJ...",
"service_role_key": "eyJ..."
},
"config": {
"project_ref": "your-project-ref"
}
}
}
json{
"name": "Full Stack App",
"providers": {
"database": {
"type": "neon",
"credentials": { "connection_string": "postgresql://..." },
"config": { "project_id": "..." }
},
"compute": {
"type": "fly",
"credentials": { "token": "FlyV1 fm2_..." },
"config": { "app_name": "my-app" }
},
"code": {
"type": "github",
"credentials": { "token": "ghp_..." },
"config": { "owner": "...", "repo": "...", "branch": "main" }
}
}
}
json{
"name": "DB Only",
"providers": {
"database": {
"type": "mongodb",
"credentials": { "connection_string": "mongodb+srv://..." },
"config": { "database": "mydb" }
}
}
}
Compute endpoints will return "No compute provider configured" — database endpoints work fine.
bash# 1. ALWAYS test connectivity first
curl -s -X POST "$BASE/stacks/test" \
-H "Content-Type: application/json" \
-d '{"stack_id": NEW_ID}'
# 2. Full introspection
curl -s -X POST "$BASE/introspect" \
-H "Content-Type: application/json" \
-d '{"stack_id": NEW_ID}'
# 3. Start exploring
curl -s -X POST "$BASE/tables/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": NEW_ID}'
IMPORTANT: Always run stacks/test after stacks/connect. It populates config values needed for queries.
https://backend-dashboard-api.fly.dev/webhooks/githubapplication/jsonpush eventbash# Merge update — only specified providers change
curl -s -X POST "$BASE/stacks/update" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"providers": {
"compute": {
"type": "fly",
"credentials": { "token": "FlyV1 fm2_new..." },
"config": { "app_name": "my-app" }
}
}
}'# Connecting Stacks
## How Stack Connection Works
A stack is a composition of providers. When you connect a stack, you provide:
- `name` — human-readable label
- `providers` — JSON object mapping primitives to provider configs
```bash
BASE="https://backend-dashboard-api.fly.dev"
curl -s -X POST "$BASE/stacks/connect" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"name": "My Project",
"providers": {
"database": {
"type": "neon",
"credentials": {
"connection_string": "postgresql://user:pass@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"
},
"config": {
"project_id": "your-neon-project-id"
}
},
"compute": {
"type": "fly",
"credentials": {
"token": "FlyV1 fm2_..."
},
"config": {
"app_name": "your-fly-app"
}
}
}
}'
```
Response includes `stack_id` — use this for all subsequent calls.
**Note**: `api_key`/`api_token` in credentials are auto-normalized to `token` on connect.
---
## Provider Templates
### Neon (Database)
```json
{
"database": {
"type": "neon",
"credentials": {
"connection_string": "postgresql://user:pass@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"
},
"config": {
"project_id": "muddy-dust-70287352"
}
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.connection_string` | **YES** | Primary credential for SQL queries |
| `config.project_id` | Yes | For Neon Management API |
| `config.sql_url` | Auto | Discovered by `stacks/test` |
### Fly (Compute)
```json
{
"compute": {
"type": "fly",
"credentials": {
"token": "FlyV1 fm2_lJPE..."
},
"config": {
"app_name": "my-fly-app",
"base_url": "https://my-fly-app.fly.dev"
}
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.token` | **YES** | Fly deploy token (1-year). Created with `fly tokens create deploy -x 8760h` |
| `config.app_name` | **YES** | The Fly app name |
| `config.base_url` | Recommended | Full URL to the app |
### MongoDB (Database)
```json
{
"database": {
"type": "mongodb",
"credentials": {
"connection_string": "mongodb+srv://user:pass@cluster.mongodb.net/"
},
"config": {
"database": "my_database",
"cluster": "cluster-name"
}
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.connection_string` | **YES** | mongodb+srv:// connection string |
| `config.database` | Yes | Database name (default: 'test') |
### Firebase/Firestore (Database)
```json
{
"database": {
"type": "firebase",
"credentials": {
"token": "ya29.OAUTH2_TOKEN..."
},
"config": {
"project_id": "my-firebase-project"
}
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.token` | **YES** | OAuth2 Bearer token |
| `config.project_id` | **YES** | Firebase project ID |
### GitHub (Code)
```json
{
"code": {
"type": "github",
"credentials": {
"token": "ghp_xxxxxxxxxxxx"
},
"config": {
"owner": "username-or-org",
"repo": "repository-name",
"branch": "main"
}
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.token` | Yes | PAT or GitHub App token |
| `config.owner` | Yes | Username or org |
| `config.repo` | Yes | Repository name — used for webhook matching |
| `config.branch` | No | Defaults to `main` |
### Supabase (Database)
```json
{
"database": {
"type": "supabase",
"credentials": {
"url": "https://your-project.supabase.co",
"anon_key": "eyJ...",
"service_role_key": "eyJ..."
},
"config": {
"project_ref": "your-project-ref"
}
}
}
```
---
## Combination Examples
### Neon + Fly + GitHub (Full Stack — Most Common)
```json
{
"name": "Full Stack App",
"providers": {
"database": {
"type": "neon",
"credentials": { "connection_string": "postgresql://..." },
"config": { "project_id": "..." }
},
"compute": {
"type": "fly",
"credentials": { "token": "FlyV1 fm2_..." },
"config": { "app_name": "my-app" }
},
"code": {
"type": "github",
"credentials": { "token": "ghp_..." },
"config": { "owner": "...", "repo": "...", "branch": "main" }
}
}
}
```
### Database Only (No Compute)
```json
{
"name": "DB Only",
"providers": {
"database": {
"type": "mongodb",
"credentials": { "connection_string": "mongodb+srv://..." },
"config": { "database": "mydb" }
}
}
}
```
Compute endpoints will return "No compute provider configured" — database endpoints work fine.
---
## After Connecting
```bash
# 1. ALWAYS test connectivity first
curl -s -X POST "$BASE/stacks/test" \
-H "Content-Type: application/json" \
-d '{"stack_id": NEW_ID}'
# 2. Full introspection
curl -s -X POST "$BASE/introspect" \
-H "Content-Type: application/json" \
-d '{"stack_id": NEW_ID}'
# 3. Start exploring
curl -s -X POST "$BASE/tables/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": NEW_ID}'
```
**IMPORTANT**: Always run `stacks/test` after `stacks/connect`. It populates config values needed for queries.
---
## Setting Up GitHub Webhook Auto-Deploy
1. Go to GitHub repo → Settings → Webhooks → Add webhook
2. Payload URL: `https://backend-dashboard-api.fly.dev/webhooks/github`
3. Content type: `application/json`
4. Events: Just the `push` event
5. Active: checked
---
## Updating Provider Credentials
```bash
# Merge update — only specified providers change
curl -s -X POST "$BASE/stacks/update" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"providers": {
"compute": {
"type": "fly",
"credentials": { "token": "FlyV1 fm2_new..." },
"config": { "app_name": "my-app" }
}
}
}'
```
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
] as const satisfies readonly RefusalCode[];
test("snappy-dashboard: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-dashboard: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
import { test } from "node:test";
import assert from "node:assert/strict";
import { HAND_CONTRACT } from "./api.ts";
import { REFUSAL_CODES, type RefusalCode } from "../snappy-settings/refusal-codes.ts";
/** RULE 33 COVERAGE, AGAINST THE ONE CLOSED TABLE ⟨lane r30, 2026-09-09⟩.
* This file used to restate a refusal row's own properties — that it has a
* `contract_slice`, that it has a `fix`, that it leaks no token — once per
* hand, 48 times, over rows that all come from the SAME object. Forty-eight
* copies of one check is the duplicate road the closed table exists to end:
* `snappy-settings/refusal-codes.test.ts` runs those checks ONCE over every
* row, and the second test below proves this hand carries THE ROW ITSELF and
* not a copy — an identity a drifted duplicate cannot fake. Before this, each
* hand hand-wrote its own row, and the wording had already drifted: the
* inline `unknown_verb` said "Call one of the verbs named in
* HAND_CONTRACT.verbs" while the closed table says "Call one of the verbs the
* contract declares; the refusal lists them."
*
* DECLARED stays a literal list, deliberately. It is this hand's coverage
* manifest and it is what rule 33's lint reads out of the test SOURCE to ask
* "did a person look at this code" — deriving it from Object.keys would make
* the test pass for a hand with no refusals at all. `satisfies readonly
* RefusalCode[]` makes the compiler refuse a name the one table does not
* have. It NAMES codes; it no longer DEFINES them. */
const DECLARED = [
"unknown_verb",
] as const satisfies readonly RefusalCode[];
test("snappy-dashboard: the refusal table declares exactly the codes this test names", () => {
assert.deepEqual(Object.keys(HAND_CONTRACT.refusals ?? {}).sort(), [...DECLARED].sort());
});
test("snappy-dashboard: every declared refusal is the one closed table's own row, never a copy", () => {
const declared = Object.entries(HAND_CONTRACT.refusals ?? {});
assert.ok(declared.length > 0, "HAND_CONTRACT.refusals is empty");
for (const [code, row] of declared) {
assert.ok(code in REFUSAL_CODES, `${code} is not a row of snappy-settings/refusal-codes.ts`);
assert.equal(row, REFUSAL_CODES[code as RefusalCode], `${code} is a copy of the closed table's row, not the row itself`);
}
});
Base URL for all examples:
bashBASE="https://backend-dashboard-api.fly.dev"
All endpoints require Authorization: Bearer <token> header (except auth/signup and auth/login).
For brevity, examples below omit the auth header. Always include it.
bash# Sign up (public)
curl -s -X POST "$BASE/auth/signup" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "secret",
"name": "User Name"
}'
# → {"success": true, "user_id": 5, "email": "...", "token": "eyJ..."}
# Login (public)
curl -s -X POST "$BASE/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "secret"
}'
# → {"success": true, "user_id": 5, "token": "eyJ..."}
# Use token on all other endpoints:
TOKEN="eyJ..."
curl -s -X POST "$BASE/tables/list" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"stack_id": 3}'
bash# Connect a new stack
curl -s -X POST "$BASE/stacks/connect" \
-H "Content-Type: application/json" \
-d '{
"name": "My Stack",
"providers": {
"database": {"type": "neon", "credentials": {"connection_string": "postgresql://..."}, "config": {"project_id": "..."}},
"compute": {"type": "fly", "credentials": {"token": "FlyV1 fm2_..."}, "config": {"app_name": "my-app"}}
}
}'
# Note: api_key/api_token auto-normalized to "token" on connect
# List all stacks (filtered to your stacks only)
curl -s -X POST "$BASE/stacks/list" \
-H "Authorization: Bearer $TOKEN"
# Test connectivity (also discovers Neon config values)
curl -s -X POST "$BASE/stacks/test" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Delete a stack
curl -s -X POST "$BASE/stacks/delete" \
-H "Content-Type: application/json" \
-d '{"stack_id": 4}'
# Full introspection (database + compute)
curl -s -X POST "$BASE/introspect" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
bash# List tables
curl -s -X POST "$BASE/tables/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Table schema
curl -s -X POST "$BASE/tables/schema" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users"}'
# Create table
curl -s -X POST "$BASE/tables/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "tasks",
"columns": [
{"name": "id", "type": "SERIAL PRIMARY KEY"},
{"name": "title", "type": "TEXT NOT NULL"},
{"name": "done", "type": "BOOLEAN DEFAULT false"},
{"name": "created_at", "type": "TIMESTAMPTZ DEFAULT NOW()"}
]
}'
# Alter table
curl -s -X POST "$BASE/tables/alter" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "tasks",
"action": "add_column",
"column_name": "priority",
"column_type": "INTEGER DEFAULT 0"
}'
# Drop table
curl -s -X POST "$BASE/tables/drop" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "temp_table"}'
# Table indexes
curl -s -X POST "$BASE/tables/indexes" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users"}'
# Table relationships
curl -s -X POST "$BASE/tables/relationships" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "orders"}'
bash# List records (paginated)
curl -s -X POST "$BASE/records/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users", "limit": 20, "offset": 0}'
# Get single record
curl -s -X POST "$BASE/records/get" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users", "id": 1}'
# Create record
curl -s -X POST "$BASE/records/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "users",
"data": {"name": "John Doe", "email": "john@example.com"}
}'
# Update record
curl -s -X POST "$BASE/records/update" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "users",
"id": 1,
"data": {"name": "Jane Doe"}
}'
# Delete record
curl -s -X POST "$BASE/records/delete" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users", "id": 1}'
# Search records
curl -s -X POST "$BASE/records/search" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "users",
"field": "email",
"value": "john",
"operator": "contains"
}'
bash# Run any SQL query
curl -s -X POST "$BASE/sql/query" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "sql": "SELECT * FROM users ORDER BY created_at DESC LIMIT 5"}'
# Run migration (DDL)
curl -s -X POST "$BASE/sql/migrate" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"sql": "ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ",
"description": "Add last_login column"
}'
bash# List machines
curl -s -X POST "$BASE/machines/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Start machine
curl -s -X POST "$BASE/machines/start" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Stop machine
curl -s -X POST "$BASE/machines/stop" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Machine config
curl -s -X POST "$BASE/machines/config" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "action": "read"}'
bash# List routes
curl -s -X POST "$BASE/routes/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Get route source code
curl -s -X POST "$BASE/routes/get" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Deploy route
curl -s -X POST "$BASE/routes/deploy" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "hello",
"method": "GET",
"path": "/hello",
"code": "res.json({msg: \"hello world\"})"
}'
# Undeploy route
curl -s -X POST "$BASE/routes/undeploy" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Route health check (detect drift across memory/filesystem/DB)
curl -s -X POST "$BASE/routes/health" \
-H "Content-Type: application/json" \
-d '{"stack_id": 6}'
# → {"healthy": true, "counts": {"memory": 70, "filesystem": 70, "database": 70}, "drift": []}
# Sync routes from DB (rebuild filesystem + memory, remove orphans)
curl -s -X POST "$BASE/routes/sync" \
-H "Content-Type: application/json" \
-d '{"stack_id": 6}'
# → {"success": true, "synced": 70, "orphans_removed": 0}
bash# List daemons
curl -s -X POST "$BASE/daemons/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Start daemon (interval-based)
curl -s -X POST "$BASE/daemons/start" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "health-check",
"code": "console.log(\"tick\")",
"interval_ms": 60000
}'
# Stop daemon
curl -s -X POST "$BASE/daemons/stop" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "health-check"}'
bash# File tree
curl -s -X POST "$BASE/code/tree" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# File tree (specific path)
curl -s -X POST "$BASE/code/tree" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "path": "endpoints"}'
# Read file
curl -s -X POST "$BASE/code/file" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "path": "package.json"}'
# Write file (commit to GitHub)
curl -s -X POST "$BASE/code/write" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"path": "README.md",
"content": "# Hello World",
"message": "Update README"
}'
# Search code
curl -s -X POST "$BASE/code/search" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "search_term": "express", "extension": "js"}'
bash# Shell exec
curl -s -X POST "$BASE/fly/exec" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "command": "ls -la /app"}'
# Fly logs
curl -s -X POST "$BASE/fly/logs" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Fly deploy (stop → update config → start → wait)
curl -s -X POST "$BASE/fly/deploy" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"env": {"NEW_VAR": "value"}
}'
# Env vars
curl -s -X POST "$BASE/env/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
bash# Deploy endpoint (creates on compute + persists to GitHub)
curl -s -X POST "$BASE/endpoints/deploy" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "hello",
"method": "GET",
"path": "/hello",
"code": "res.json({ msg: \"hello world\", time: new Date().toISOString() })"
}'
# List endpoints
curl -s -X POST "$BASE/endpoints/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Get endpoint source
curl -s -X POST "$BASE/endpoints/get" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Delete endpoint
curl -s -X POST "$BASE/endpoints/delete" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Restore all endpoints from GitHub
curl -s -X POST "$BASE/endpoints/restore" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
bash# Create group
curl -s -X POST "$BASE/groups/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "Core API",
"description": "Primary API endpoints",
"base_path": "/api/v1"
}'
# List groups
curl -s -X POST "$BASE/groups/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Get/Update/Delete groups
curl -s -X POST "$BASE/groups/get" -d '{"stack_id": 3, "group_id": 1}'
curl -s -X POST "$BASE/groups/update" -d '{"stack_id": 3, "group_id": 1, "name": "Core API v2"}'
curl -s -X POST "$BASE/groups/delete" -d '{"stack_id": 3, "group_id": 1}'
bash# Create function
curl -s -X POST "$BASE/functions/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "formatDate",
"code": "module.exports = (date) => new Date(date).toISOString().split(\"T\")[0];",
"description": "Format date to YYYY-MM-DD"
}'
# List/Get/Update/Delete functions
curl -s -X POST "$BASE/functions/list" -d '{"stack_id": 3}'
curl -s -X POST "$BASE/functions/get" -d '{"stack_id": 3, "function_id": 1}'
curl -s -X POST "$BASE/functions/update" -d '{"stack_id": 3, "function_id": 1, "code": "..."}'
curl -s -X POST "$BASE/functions/delete" -d '{"stack_id": 3, "function_id": 1}'
bash# Create task (registers + starts daemon + persists to GitHub)
curl -s -X POST "$BASE/tasks/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "cleanup-logs",
"code": "await db.query(\"DELETE FROM logs WHERE created_at < NOW() - INTERVAL '\''30 days'\''\")",
"description": "Remove old logs",
"schedule_type": "cron",
"schedule_value": "0 3 * * *"
}'
# List/Get/Update/Activate/Delete tasks
curl -s -X POST "$BASE/tasks/list" -d '{"stack_id": 3}'
curl -s -X POST "$BASE/tasks/get" -d '{"stack_id": 3, "task_id": 1}'
curl -s -X POST "$BASE/tasks/activate" -d '{"stack_id": 3, "task_id": 1, "active": true}'
curl -s -X POST "$BASE/tasks/delete" -d '{"stack_id": 3, "task_id": 1}'
bashcurl -s -X POST "$BASE/state/get" -d '{"stack_id": 3, "key": "app:version"}'
curl -s -X POST "$BASE/state/set" -d '{"stack_id": 3, "key": "app:version", "value": "2.1.0"}'
curl -s -X POST "$BASE/state/list" -d '{"stack_id": 3}'
curl -s -X POST "$BASE/state/delete" -d '{"stack_id": 3, "key": "app:version"}'
bash# Update provider credentials (merge — only specified providers change)
curl -s -X POST "$BASE/stacks/update" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"providers": {
"compute": {
"type": "fly",
"credentials": {"token": "FlyV1 fm2_new_token..."},
"config": {"app_name": "my-app"}
}
}
}'# Curl Reference — Every Endpoint
Base URL for all examples:
```bash
BASE="https://backend-dashboard-api.fly.dev"
```
**All endpoints require `Authorization: Bearer <token>` header** (except auth/signup and auth/login).
For brevity, examples below omit the auth header. Always include it.
---
## Authentication
```bash
# Sign up (public)
curl -s -X POST "$BASE/auth/signup" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "secret",
"name": "User Name"
}'
# → {"success": true, "user_id": 5, "email": "...", "token": "eyJ..."}
# Login (public)
curl -s -X POST "$BASE/auth/login" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "secret"
}'
# → {"success": true, "user_id": 5, "token": "eyJ..."}
# Use token on all other endpoints:
TOKEN="eyJ..."
curl -s -X POST "$BASE/tables/list" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"stack_id": 3}'
```
---
## Stack Lifecycle
```bash
# Connect a new stack
curl -s -X POST "$BASE/stacks/connect" \
-H "Content-Type: application/json" \
-d '{
"name": "My Stack",
"providers": {
"database": {"type": "neon", "credentials": {"connection_string": "postgresql://..."}, "config": {"project_id": "..."}},
"compute": {"type": "fly", "credentials": {"token": "FlyV1 fm2_..."}, "config": {"app_name": "my-app"}}
}
}'
# Note: api_key/api_token auto-normalized to "token" on connect
# List all stacks (filtered to your stacks only)
curl -s -X POST "$BASE/stacks/list" \
-H "Authorization: Bearer $TOKEN"
# Test connectivity (also discovers Neon config values)
curl -s -X POST "$BASE/stacks/test" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Delete a stack
curl -s -X POST "$BASE/stacks/delete" \
-H "Content-Type: application/json" \
-d '{"stack_id": 4}'
# Full introspection (database + compute)
curl -s -X POST "$BASE/introspect" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
```
---
## Table Management
```bash
# List tables
curl -s -X POST "$BASE/tables/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Table schema
curl -s -X POST "$BASE/tables/schema" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users"}'
# Create table
curl -s -X POST "$BASE/tables/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "tasks",
"columns": [
{"name": "id", "type": "SERIAL PRIMARY KEY"},
{"name": "title", "type": "TEXT NOT NULL"},
{"name": "done", "type": "BOOLEAN DEFAULT false"},
{"name": "created_at", "type": "TIMESTAMPTZ DEFAULT NOW()"}
]
}'
# Alter table
curl -s -X POST "$BASE/tables/alter" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "tasks",
"action": "add_column",
"column_name": "priority",
"column_type": "INTEGER DEFAULT 0"
}'
# Drop table
curl -s -X POST "$BASE/tables/drop" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "temp_table"}'
# Table indexes
curl -s -X POST "$BASE/tables/indexes" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users"}'
# Table relationships
curl -s -X POST "$BASE/tables/relationships" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "orders"}'
```
---
## Record Operations
```bash
# List records (paginated)
curl -s -X POST "$BASE/records/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users", "limit": 20, "offset": 0}'
# Get single record
curl -s -X POST "$BASE/records/get" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users", "id": 1}'
# Create record
curl -s -X POST "$BASE/records/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "users",
"data": {"name": "John Doe", "email": "john@example.com"}
}'
# Update record
curl -s -X POST "$BASE/records/update" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "users",
"id": 1,
"data": {"name": "Jane Doe"}
}'
# Delete record
curl -s -X POST "$BASE/records/delete" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "table": "users", "id": 1}'
# Search records
curl -s -X POST "$BASE/records/search" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"table": "users",
"field": "email",
"value": "john",
"operator": "contains"
}'
```
---
## SQL
```bash
# Run any SQL query
curl -s -X POST "$BASE/sql/query" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "sql": "SELECT * FROM users ORDER BY created_at DESC LIMIT 5"}'
# Run migration (DDL)
curl -s -X POST "$BASE/sql/migrate" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"sql": "ALTER TABLE users ADD COLUMN last_login TIMESTAMPTZ",
"description": "Add last_login column"
}'
```
---
## Machine Management (Fly)
```bash
# List machines
curl -s -X POST "$BASE/machines/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Start machine
curl -s -X POST "$BASE/machines/start" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Stop machine
curl -s -X POST "$BASE/machines/stop" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Machine config
curl -s -X POST "$BASE/machines/config" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "action": "read"}'
```
---
## Route Management (Fly Compute)
```bash
# List routes
curl -s -X POST "$BASE/routes/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Get route source code
curl -s -X POST "$BASE/routes/get" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Deploy route
curl -s -X POST "$BASE/routes/deploy" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "hello",
"method": "GET",
"path": "/hello",
"code": "res.json({msg: \"hello world\"})"
}'
# Undeploy route
curl -s -X POST "$BASE/routes/undeploy" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Route health check (detect drift across memory/filesystem/DB)
curl -s -X POST "$BASE/routes/health" \
-H "Content-Type: application/json" \
-d '{"stack_id": 6}'
# → {"healthy": true, "counts": {"memory": 70, "filesystem": 70, "database": 70}, "drift": []}
# Sync routes from DB (rebuild filesystem + memory, remove orphans)
curl -s -X POST "$BASE/routes/sync" \
-H "Content-Type: application/json" \
-d '{"stack_id": 6}'
# → {"success": true, "synced": 70, "orphans_removed": 0}
```
---
## Daemon Management
```bash
# List daemons
curl -s -X POST "$BASE/daemons/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Start daemon (interval-based)
curl -s -X POST "$BASE/daemons/start" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "health-check",
"code": "console.log(\"tick\")",
"interval_ms": 60000
}'
# Stop daemon
curl -s -X POST "$BASE/daemons/stop" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "health-check"}'
```
---
## Code Operations (GitHub)
```bash
# File tree
curl -s -X POST "$BASE/code/tree" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# File tree (specific path)
curl -s -X POST "$BASE/code/tree" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "path": "endpoints"}'
# Read file
curl -s -X POST "$BASE/code/file" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "path": "package.json"}'
# Write file (commit to GitHub)
curl -s -X POST "$BASE/code/write" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"path": "README.md",
"content": "# Hello World",
"message": "Update README"
}'
# Search code
curl -s -X POST "$BASE/code/search" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "search_term": "express", "extension": "js"}'
```
---
## Fly Operations
```bash
# Shell exec
curl -s -X POST "$BASE/fly/exec" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "command": "ls -la /app"}'
# Fly logs
curl -s -X POST "$BASE/fly/logs" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Fly deploy (stop → update config → start → wait)
curl -s -X POST "$BASE/fly/deploy" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"env": {"NEW_VAR": "value"}
}'
# Env vars
curl -s -X POST "$BASE/env/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
```
---
## Endpoint Orchestration (compute + code)
```bash
# Deploy endpoint (creates on compute + persists to GitHub)
curl -s -X POST "$BASE/endpoints/deploy" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "hello",
"method": "GET",
"path": "/hello",
"code": "res.json({ msg: \"hello world\", time: new Date().toISOString() })"
}'
# List endpoints
curl -s -X POST "$BASE/endpoints/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Get endpoint source
curl -s -X POST "$BASE/endpoints/get" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Delete endpoint
curl -s -X POST "$BASE/endpoints/delete" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3, "name": "hello"}'
# Restore all endpoints from GitHub
curl -s -X POST "$BASE/endpoints/restore" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
```
---
## API Groups
```bash
# Create group
curl -s -X POST "$BASE/groups/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "Core API",
"description": "Primary API endpoints",
"base_path": "/api/v1"
}'
# List groups
curl -s -X POST "$BASE/groups/list" \
-H "Content-Type: application/json" \
-d '{"stack_id": 3}'
# Get/Update/Delete groups
curl -s -X POST "$BASE/groups/get" -d '{"stack_id": 3, "group_id": 1}'
curl -s -X POST "$BASE/groups/update" -d '{"stack_id": 3, "group_id": 1, "name": "Core API v2"}'
curl -s -X POST "$BASE/groups/delete" -d '{"stack_id": 3, "group_id": 1}'
```
---
## Functions
```bash
# Create function
curl -s -X POST "$BASE/functions/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "formatDate",
"code": "module.exports = (date) => new Date(date).toISOString().split(\"T\")[0];",
"description": "Format date to YYYY-MM-DD"
}'
# List/Get/Update/Delete functions
curl -s -X POST "$BASE/functions/list" -d '{"stack_id": 3}'
curl -s -X POST "$BASE/functions/get" -d '{"stack_id": 3, "function_id": 1}'
curl -s -X POST "$BASE/functions/update" -d '{"stack_id": 3, "function_id": 1, "code": "..."}'
curl -s -X POST "$BASE/functions/delete" -d '{"stack_id": 3, "function_id": 1}'
```
---
## Tasks
```bash
# Create task (registers + starts daemon + persists to GitHub)
curl -s -X POST "$BASE/tasks/create" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"name": "cleanup-logs",
"code": "await db.query(\"DELETE FROM logs WHERE created_at < NOW() - INTERVAL '\''30 days'\''\")",
"description": "Remove old logs",
"schedule_type": "cron",
"schedule_value": "0 3 * * *"
}'
# List/Get/Update/Activate/Delete tasks
curl -s -X POST "$BASE/tasks/list" -d '{"stack_id": 3}'
curl -s -X POST "$BASE/tasks/get" -d '{"stack_id": 3, "task_id": 1}'
curl -s -X POST "$BASE/tasks/activate" -d '{"stack_id": 3, "task_id": 1, "active": true}'
curl -s -X POST "$BASE/tasks/delete" -d '{"stack_id": 3, "task_id": 1}'
```
---
## State Management
```bash
curl -s -X POST "$BASE/state/get" -d '{"stack_id": 3, "key": "app:version"}'
curl -s -X POST "$BASE/state/set" -d '{"stack_id": 3, "key": "app:version", "value": "2.1.0"}'
curl -s -X POST "$BASE/state/list" -d '{"stack_id": 3}'
curl -s -X POST "$BASE/state/delete" -d '{"stack_id": 3, "key": "app:version"}'
```
---
## Stack Update
```bash
# Update provider credentials (merge — only specified providers change)
curl -s -X POST "$BASE/stacks/update" \
-H "Content-Type: application/json" \
-d '{
"stack_id": 3,
"providers": {
"compute": {
"type": "fly",
"credentials": {"token": "FlyV1 fm2_new_token..."},
"config": {"app_name": "my-app"}
}
}
}'
```
Production-verified gotchas from building and debugging Snappy Dashboard. Every item here caused real failures.
The stacks/connect and stacks/update endpoints auto-normalize incoming provider credentials:
api_key → tokenapi_token → tokenSo you can pass api_key or api_token and it gets stored as token. But when READING providers back, always expect credentials.token.
| Provider | Credentials | Config |
|---|---|---|
| Fly | token (FlyV1 deploy token) |
app_name, base_url, org |
| Neon | connection_string (primary) |
sql_url (HTTP), project_id, host |
| MongoDB | connection_string (mongodb+srv://) |
database, cluster |
| Firebase | token (OAuth2 Bearer) |
project_id |
| GitHub | token (PAT or App token) |
owner, repo, branch |
All compute endpoints auto-start stopped machines:
state=started (up to 30s)Cold start: ~10-15s. Warm: ~1s.
Common failure: Wrong Fly token → ensure_fly_running fails with opaque error. Verify stacks/test returns compute: {ok: true} first.
fly tokens create deploy -x 8760h (1-year).fly auth token) DO rotate and expire.body shorthand#In route code, there's NO body variable. Use req.body.
json() shorthand#There's NO standalone json() function. Use res.json() and res.status(code).json().
env.*#process.env only has PORT. Use getSecret("KEY_NAME") for secrets (if available on the compute).
Routes are separate modules loaded via require(). callFunction is module-scoped in server.js. Fix: app.locals.callFunction exposes it, and routes access via callFn(name, ...args).
POST https://backend-dashboard-api.fly.dev/webhooks/githubrepository.name from payloadproviders.code.config.repo in all stacksendpoints/*.json files from GitHubrepo field must exactly match GitHub repo name (case-sensitive)endpoints/ directoryAll CRUD routes support 3 DB types: Postgres/Neon/Supabase (SQL), MongoDB (native driver), Firebase (REST).
require('mongodb') — needs to be in hp-base package.jsoncredentials.connection_string, config.database (default: 'test')config.project_id, credentials.token (OAuth2)table || table_name || collection for compatibilityRoutes live in 3 stores: Neon DB (_route_store), filesystem (/app/routes/*.js), and in-memory Map (deployedRoutes). They used to drift silently — DB writes were fire-and-forget, undeploy skipped DB if the file was missing, and multi-machine deploys only hit one machine.
deployRoute() is async and awaits the DB write before touching filesystem. If DB fails, nothing else happens.saveFunction() and deleteFunction() now await DB writes (was fire-and-forget).initRouteStore() writes all route files first, calls loadRoutes() once (was O(N²) — each route triggered a full reload).ALTER TABLE ADD COLUMN IF NOT EXISTS language TEXT DEFAULT 'js' runs on every init.{db: true/false, file: true/false}.POST /_sync: Reads all routes from DB, deploys to filesystem, removes orphan files. Use after deploys or to fix drift.GET /_health/routes: Compares memory vs filesystem vs DB. Returns {healthy: bool, counts, drift}.POST /_reload: Now syncs from DB when available (not just filesystem).hp-base@5.0.0 from npm. Bug fixes apply to all at once.routes_sync: Calls POST /routes/sync → proxies to target stack's /_syncroutes_health: Calls POST /routes/health → proxies to target stack's /_health/routesFly load balancer routes deploys to ONE machine. The other machine is stale until:
initRouteStore())routes_sync on it/_reload on it (now does DB sync)Workflow for multi-machine stacks: After deploying, call routes_sync to ensure all machines match. Or just use endpoints_deploy which will be synced on next restart.
routes_health — does it show healthy: true?routes_sync to reconcileroutes_undeploy, it always cleans DB regardless of filesystemroutes_sync removes them automatically| Feature | endpoints/deploy |
routes/deploy |
|---|---|---|
| Persists to GitHub | YES | NO |
| Deploys to compute | YES | YES |
| Persists to DB | YES (ub_endpoints + _route_store) | YES (_route_store, DB-first) |
| Survives cold-start | YES (restored from DB) | YES (restored from _route_store) |
| Version history | YES (ub_endpoint_versions) | NO |
| Use for | Production endpoints | Quick testing |
When something "doesn't work" on a stack:
routes_health — are memory/filesystem/DB in sync?routes_sync to reconcile from DBstacks/test — does each provider return ok: true?token?machines/listfly/logs or fly logs -a app-name from CLI# Hard-Won Lessons
Production-verified gotchas from building and debugging Snappy Dashboard. Every item here caused real failures.
---
## Provider Credential Field Names
### Auto-Normalization on Connect
The `stacks/connect` and `stacks/update` endpoints auto-normalize incoming provider credentials:
- `api_key` → `token`
- `api_token` → `token`
So you can pass `api_key` or `api_token` and it gets stored as `token`. But when READING providers back, always expect `credentials.token`.
### Canonical Field Names (Production)
| Provider | Credentials | Config |
|----------|-------------|--------|
| **Fly** | `token` (FlyV1 deploy token) | `app_name`, `base_url`, `org` |
| **Neon** | `connection_string` (primary) | `sql_url` (HTTP), `project_id`, `host` |
| **MongoDB** | `connection_string` (mongodb+srv://) | `database`, `cluster` |
| **Firebase** | `token` (OAuth2 Bearer) | `project_id` |
| **GitHub** | `token` (PAT or App token) | `owner`, `repo`, `branch` |
---
## Platform vs Tenant Architecture (CRITICAL)
- **Stack 3 = THE PLATFORM** ("Backend Dashboard"). All routes, auth, _route_store, ub_stacks table live here.
- **All other stacks are TENANTS** — user data the platform operates on.
- Breaking stack 3 breaks the ENTIRE system. Route deploys go to stack 3's compute.
- Tenant stacks may have only a database provider (no compute, no code). That's fine.
- **Never modify stack 3's database schema casually.**
---
## Fly Machine Auto-Start
All compute endpoints auto-start stopped machines:
1. Check machine state via Fly Machines API
2. If stopped → start (Fly auto-stops idle machines)
3. Wait for `state=started` (up to 30s)
4. Health-check loops until HTTP ready
5. Then the actual operation proceeds
**Cold start**: ~10-15s. **Warm**: ~1s.
**Common failure**: Wrong Fly token → `ensure_fly_running` fails with opaque error. Verify `stacks/test` returns `compute: {ok: true}` first.
---
## Fly Deploy Token vs Macaroon Token
- **Deploy tokens** don't rotate. Created with `fly tokens create deploy -x 8760h` (1-year).
- **Macaroon tokens** (from `fly auth token`) DO rotate and expire.
- Always use deploy tokens for stack credentials.
---
## Route Sandbox Gotchas
### No `body` shorthand
In route code, there's NO `body` variable. Use `req.body`.
### No `json()` shorthand
There's NO standalone `json()` function. Use `res.json()` and `res.status(code).json()`.
### No `env.*`
`process.env` only has `PORT`. Use `getSecret("KEY_NAME")` for secrets (if available on the compute).
### callFn requires v3.4.0+
Routes are separate modules loaded via `require()`. `callFunction` is module-scoped in server.js. Fix: `app.locals.callFunction` exposes it, and routes access via `callFn(name, ...args)`.
---
## GitHub Webhook Pipeline
### How it works
1. GitHub push → `POST https://backend-dashboard-api.fly.dev/webhooks/github`
2. Reads `repository.name` from payload
3. Matches against `providers.code.config.repo` in all stacks
4. Reads `endpoints/*.json` files from GitHub
5. Auto-starts Fly machine
6. Deploys each endpoint
### Common failures
- **No stack matches**: `repo` field must exactly match GitHub repo name (case-sensitive)
- **Fly token invalid**: Deployment fails silently
- **Non-JSON files**: Skipped in `endpoints/` directory
---
## Multi-Database Support
All CRUD routes support 3 DB types: **Postgres/Neon/Supabase** (SQL), **MongoDB** (native driver), **Firebase** (REST).
- MongoDB uses `require('mongodb')` — needs to be in hp-base `package.json`
- MongoDB stacks need: `credentials.connection_string`, `config.database` (default: 'test')
- Firebase stacks need: `config.project_id`, `credentials.token` (OAuth2)
- All routes accept `table || table_name || collection` for compatibility
---
## Route Consistency — DB as Source of Truth
### The Problem (Pre-Fix)
Routes live in 3 stores: Neon DB (`_route_store`), filesystem (`/app/routes/*.js`), and in-memory Map (`deployedRoutes`). They used to drift silently — DB writes were fire-and-forget, undeploy skipped DB if the file was missing, and multi-machine deploys only hit one machine.
### The Fix (hp-base v5.0.0)
- **DB-first deploys**: `deployRoute()` is `async` and `await`s the DB write before touching filesystem. If DB fails, nothing else happens.
- **DB-first functions**: `saveFunction()` and `deleteFunction()` now `await` DB writes (was fire-and-forget).
- **O(N) batch init**: `initRouteStore()` writes all route files first, calls `loadRoutes()` once (was O(N²) — each route triggered a full reload).
- **Graceful shutdown**: SIGTERM/SIGINT handlers stop daemons, drain connections, close pool.
- **Language column migration**: `ALTER TABLE ADD COLUMN IF NOT EXISTS language TEXT DEFAULT 'js'` runs on every init.
- **Undeploy always cleans DB**: Even if the file doesn't exist on this machine, the DB row gets deleted. Returns `{db: true/false, file: true/false}`.
- **`POST /_sync`**: Reads all routes from DB, deploys to filesystem, removes orphan files. Use after deploys or to fix drift.
- **`GET /_health/routes`**: Compares memory vs filesystem vs DB. Returns `{healthy: bool, counts, drift}`.
- **`POST /_reload`**: Now syncs from DB when available (not just filesystem).
- **Shared npm package**: All 3 stacks (ub-server, content-engine, total-crm) use `hp-base@5.0.0` from npm. Bug fixes apply to all at once.
### MCP Tools
- **`routes_sync`**: Calls `POST /routes/sync` → proxies to target stack's `/_sync`
- **`routes_health`**: Calls `POST /routes/health` → proxies to target stack's `/_health/routes`
### Multi-Machine Drift
Fly load balancer routes deploys to ONE machine. The other machine is stale until:
1. It restarts (reads from DB via `initRouteStore()`)
2. You call `routes_sync` on it
3. You call `/_reload` on it (now does DB sync)
**Workflow for multi-machine stacks**: After deploying, call `routes_sync` to ensure all machines match. Or just use `endpoints_deploy` which will be synced on next restart.
### Debugging Route Issues
1. **Run `routes_health`** — does it show `healthy: true`?
2. **If drift detected** — run `routes_sync` to reconcile
3. **Ghost routes** (exist in DB but not wanted) — use `routes_undeploy`, it always cleans DB regardless of filesystem
4. **Orphan files** (on disk but not in DB) — `routes_sync` removes them automatically
---
## Endpoint Deploy vs Route Deploy
| Feature | `endpoints/deploy` | `routes/deploy` |
|---------|-------------------|-----------------|
| Persists to GitHub | YES | NO |
| Deploys to compute | YES | YES |
| Persists to DB | YES (ub_endpoints + _route_store) | YES (_route_store, DB-first) |
| Survives cold-start | YES (restored from DB) | YES (restored from _route_store) |
| Version history | YES (ub_endpoint_versions) | NO |
| Use for | Production endpoints | Quick testing |
---
## Debugging Checklist
When something "doesn't work" on a stack:
1. **Run `routes_health`** — are memory/filesystem/DB in sync?
2. **If drift** — run `routes_sync` to reconcile from DB
3. **Run `stacks/test`** — does each provider return `ok: true`?
4. **Check field names** — is the credential stored as `token`?
5. **Check machine state** — is the Fly machine running? Try `machines/list`
6. **Check Fly logs** — `fly/logs` or `fly logs -a app-name` from CLI
7. **Check stack_id** — are you targeting the right stack?
8. **Check auth** — is your JWT expired? (7-day expiry)
Each provider type has specific credential and config fields. This document shows the EXACT JSON structure expected.
CRITICAL: stacks/connect auto-normalizes api_key/api_token → token. When reading back, always expect credentials.token.
json{
"type": "neon",
"credentials": {
"connection_string": "postgresql://user:pass@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"
},
"config": {
"project_id": "muddy-dust-70287352",
"sql_url": "https://ep-xxx.region.aws.neon.tech/sql"
}
}
| Field | Required | Notes |
|---|---|---|
credentials.connection_string |
YES | Primary credential for SQL queries |
config.project_id |
Yes | For Neon Management API |
config.sql_url |
Auto | Discovered by stacks/test — SQL-over-HTTP endpoint |
How queries work: SQL-over-HTTP at POST {sql_url} with Neon-Connection-String: {connection_string} header.
json{
"type": "mongodb",
"credentials": {
"connection_string": "mongodb+srv://user:pass@cluster.mongodb.net/"
},
"config": {
"database": "my_database",
"cluster": "cluster-name"
}
}
| Field | Required | Notes |
|---|---|---|
credentials.connection_string |
YES | mongodb+srv:// connection string |
config.database |
Yes | Database name (default: 'test') |
config.cluster |
No | Cluster name for reference |
How queries work: Native mongodb v6 driver connects with the connection string.
json{
"type": "firebase",
"credentials": {
"token": "ya29.OAUTH2_TOKEN..."
},
"config": {
"project_id": "my-firebase-project"
}
}
| Field | Required | Notes |
|---|---|---|
credentials.token |
YES | OAuth2 Bearer token |
config.project_id |
YES | Firebase project ID |
How queries work: Firestore REST API at https://firestore.googleapis.com/v1/projects/{project_id}/databases/(default)/documents/...
json{
"type": "supabase",
"credentials": {
"url": "https://your-project.supabase.co",
"anon_key": "eyJ...",
"service_role_key": "eyJ..."
},
"config": {
"project_ref": "abcdefghijklmnop"
}
}
json{
"type": "fly",
"credentials": {
"token": "FlyV1 fm2_lJPE..."
},
"config": {
"app_name": "my-fly-app",
"base_url": "https://my-fly-app.fly.dev",
"org": "personal"
}
}
| Field | Required | Notes |
|---|---|---|
credentials.token |
YES | Fly deploy token (1-year). NOT macaroon token. |
config.app_name |
YES | The Fly app name |
config.base_url |
Recommended | Full URL to the app |
config.org |
No | Fly organization |
Create deploy token: fly tokens create deploy -x 8760h (1-year, doesn't rotate like macaroon tokens)
How compute works: REST API at https://api.machines.dev/v1/apps/{app_name}/machines/... with Authorization: Bearer {token}. Routes deploy to https://{app_name}.fly.dev/_deploy.
json{
"type": "github",
"credentials": {
"token": "ghp_xxxxxxxxxxxx"
},
"config": {
"owner": "roboulos",
"repo": "my-repo",
"branch": "main"
}
}
| Field | Required | Notes |
|---|---|---|
credentials.token |
Yes | PAT (ghp_...) or GitHub App token (ghs_...) |
config.owner |
Yes | Username or organization |
config.repo |
Yes | Repository name — used for webhook matching |
config.branch |
No | Defaults to main |
When you call stacks/test, it:
sql_url, connection_string, pooler_host from Management APIlistDatabasesAlways run stacks/test after stacks/connect — it populates config values needed for queries.
# Provider Formats
Each provider type has specific credential and config fields. This document shows the EXACT JSON structure expected.
**CRITICAL: `stacks/connect` auto-normalizes `api_key`/`api_token` → `token`. When reading back, always expect `credentials.token`.**
---
## Database Providers
### Neon (Postgres)
```json
{
"type": "neon",
"credentials": {
"connection_string": "postgresql://user:pass@ep-xxx.region.aws.neon.tech/neondb?sslmode=require"
},
"config": {
"project_id": "muddy-dust-70287352",
"sql_url": "https://ep-xxx.region.aws.neon.tech/sql"
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.connection_string` | **YES** | Primary credential for SQL queries |
| `config.project_id` | Yes | For Neon Management API |
| `config.sql_url` | Auto | Discovered by `stacks/test` — SQL-over-HTTP endpoint |
**How queries work**: SQL-over-HTTP at `POST {sql_url}` with `Neon-Connection-String: {connection_string}` header.
### MongoDB
```json
{
"type": "mongodb",
"credentials": {
"connection_string": "mongodb+srv://user:pass@cluster.mongodb.net/"
},
"config": {
"database": "my_database",
"cluster": "cluster-name"
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.connection_string` | **YES** | mongodb+srv:// connection string |
| `config.database` | Yes | Database name (default: 'test') |
| `config.cluster` | No | Cluster name for reference |
**How queries work**: Native `mongodb` v6 driver connects with the connection string.
### Firebase/Firestore
```json
{
"type": "firebase",
"credentials": {
"token": "ya29.OAUTH2_TOKEN..."
},
"config": {
"project_id": "my-firebase-project"
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.token` | **YES** | OAuth2 Bearer token |
| `config.project_id` | **YES** | Firebase project ID |
**How queries work**: Firestore REST API at `https://firestore.googleapis.com/v1/projects/{project_id}/databases/(default)/documents/...`
### Supabase
```json
{
"type": "supabase",
"credentials": {
"url": "https://your-project.supabase.co",
"anon_key": "eyJ...",
"service_role_key": "eyJ..."
},
"config": {
"project_ref": "abcdefghijklmnop"
}
}
```
---
## Compute Providers
### Fly
```json
{
"type": "fly",
"credentials": {
"token": "FlyV1 fm2_lJPE..."
},
"config": {
"app_name": "my-fly-app",
"base_url": "https://my-fly-app.fly.dev",
"org": "personal"
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.token` | **YES** | Fly deploy token (1-year). NOT macaroon token. |
| `config.app_name` | **YES** | The Fly app name |
| `config.base_url` | Recommended | Full URL to the app |
| `config.org` | No | Fly organization |
**Create deploy token**: `fly tokens create deploy -x 8760h` (1-year, doesn't rotate like macaroon tokens)
**How compute works**: REST API at `https://api.machines.dev/v1/apps/{app_name}/machines/...` with `Authorization: Bearer {token}`. Routes deploy to `https://{app_name}.fly.dev/_deploy`.
---
## Code Providers
### GitHub
```json
{
"type": "github",
"credentials": {
"token": "ghp_xxxxxxxxxxxx"
},
"config": {
"owner": "roboulos",
"repo": "my-repo",
"branch": "main"
}
}
```
| Field | Required | Notes |
|-------|----------|-------|
| `credentials.token` | Yes | PAT (`ghp_...`) or GitHub App token (`ghs_...`) |
| `config.owner` | Yes | Username or organization |
| `config.repo` | Yes | Repository name — used for webhook matching |
| `config.branch` | No | Defaults to `main` |
---
## Provider Discovery via stacks/test
When you call `stacks/test`, it:
1. Tests connectivity for each configured provider
2. For **Neon**: discovers `sql_url`, `connection_string`, `pooler_host` from Management API
3. For **Fly**: tests machines API, returns machine list and states
4. For **MongoDB**: tests connection with `listDatabases`
5. For **Firebase**: tests Firestore API access
6. For **GitHub**: tests repo access with the token
7. Updates stack status to "connected" or "error"
**Always run `stacks/test` after `stacks/connect`** — it populates config values needed for queries.