A multi-tenant web app on Cloudflare that reads the plans/ directories across
your GitHub repos (the ones that use the plans
skill) and lets you browse them by state — backlog, ready, in-progress, done —
in one place. GitHub is the source of truth; the app only ever reads and (in
later phases) writes plans through the GitHub API.
The plans skill lives in this repo too — it's the spec this app reads, so
the two share one source of truth for the lifecycle. See
The plans skill below.
This repo currently implements Phase 0 (foundations), Phase 1 (the read-only reader), Phase 2 (hand editing), and Phase 3 (AI-assisted moves) — plans can be edited and committed back as bot-authored commits, and moved between lifecycle states with Claude (via Cloudflare AI Gateway) drafting the rewrite, shown as a diff to approve before an atomic move-and-update commit. AI-drafted new backlog items and Flue chat are later phases — see the plan.
- Sign in with GitHub (GitHub App user-OAuth), signed HttpOnly session cookies.
- Installation linking — at login the app snapshots which App installations you can access and stores the user→installation mapping in D1.
- Automatic repo discovery — scans each installation's repos for a top-level
plans/folder (Git Trees API), caching results in D1. - Dashboard — installations, and under each, the repos that have plans.
- Repo view — the four states as lists, populated from the plans.
- Plan view — rendered markdown body + parsed frontmatter (title, status, dates).
- Freshness — incremental cache keyed by git blob sha, a manual "Rescan"/
"Refresh", and
push/installationwebhook handling. - One-command bootstrap —
curl -fsSL plans.superhighfives.com/start | shturns any repo into a plans-enabled repo (see Bootstrapping a repo).
Only top-level plans/<state>/*.md files with valid frontmatter (a non-empty
title) are recognized; anything else is silently skipped ("detect and skip").
Turn any repo into a plans-enabled repo with one command — no npx, no auth,
just this app:
curl -fsSL plans.superhighfives.com/start | shIt installs the plans skill for your agents — both .agents/skills/plans/
(the shared convention opencode et al. read) and .claude/skills/plans/ (Claude
Code) — adds a plans section to AGENTS.md, and creates
plans/{backlog,ready,in-progress,done}/ with a seeded README.md. It's
idempotent and never clobbers existing files, so re-running it is safe. Working
with an agent, just say "run plans.superhighfives.com/start in this repo" —
it runs the same curl.
Refresh the skill later without re-bootstrapping:
curl -fsSL plans.superhighfives.com/start | sh -s -- updateVisiting the URL in a browser (or an agent fetching it) shows a readable page
instead of the script — /start content-negotiates on Accept. The app serves
its own skill at /start/skill, so /start has no dependency beyond the app.
See plans/done/start-endpoint.md for the design.
skills/plans/SKILL.md is the agent skill that
writes the plans/ structure this app reads — the four states, the naming
convention, the frontmatter schema, and the /plans slash-command modes. It
lives here (rather than in a general skills collection) because the skill and the
app are two halves of one contract: change the lifecycle in one and you change it
in the other.
To keep that from drifting, the contract has a single source of truth in code —
src/lib/plans/states.ts (PLAN_STATE_DEFS), from
which the state ids, UI labels, frontmatter status values, and the
plans/<state>/*.md path matcher are all derived. states.skill.test.ts reads
SKILL.md and fails if its documented lifecycle or status enum no longer matches
PLAN_STATE_DEFS.
Install the skill globally with the skills
CLI:
npx skills add superhighfives/plans --skill plans -g- TanStack Start (React 19) on Cloudflare Workers — server functions are the API boundary; tokens never reach the browser.
- Cloudflare D1 (SQLite) + Drizzle for app state and caches.
- WebCrypto for the App JWT (RS256), installation-token encryption (AES-256-GCM), and cookie signing (HMAC).
Browser (TanStack Start / React)
│ route loaders → server functions (RPC)
▼
Cloudflare Worker
├── GitHub App → installation tokens → repo contents + PRs (read)
├── D1 (Drizzle) → users, installations, repos, plan cache, audit
└── WebCrypto → App JWT, token encryption, cookie signing
Source of truth is GitHub. D1 is a cache, rebuilt on demand and validated
against git blob shas; push webhooks evict stale entries.
src/
env.ts Typed Worker bindings/secrets (per-request only)
router.tsx Router factory
db/ Drizzle schema + client
lib/
crypto.ts WebCrypto: base64(url), HMAC, AES-GCM, ids
github/ App JWT, REST client, installation tokens, OAuth, tree/blob reads
plans/ Frontmatter parser, state/path rules, shared types
server/ Server functions (RPC) + server-only helpers
session.ts Signed cookie sessions + OAuth state
*.functions.ts The RPC boundary (auth-guarded)
*.server.ts DB/GitHub logic
routes/
__root.tsx Document shell + header
index.tsx Dashboard / landing
repos/$owner/$repo/ Repo view + plan view
api/ OAuth login/callback, logout, GitHub webhook (server routes)
migrations/ D1 SQL migrations (drizzle-kit)
skills/
plans/SKILL.md The plans skill — the plans/ contract this app reads
Create one App (Settings → Developer settings → GitHub Apps → New):
- Callback URL:
https://plans.superhighfives.com/api/auth/github/callback(andhttp://localhost:5173/api/auth/github/callbackfor local dev). - Webhook URL:
https://plans.superhighfives.com/api/webhooks/github, with a webhook secret. - Permissions: Repository Contents: Read-only, Metadata: Read-only, Pull requests: Read-only (the last powers branch/PR plan activity on the board and the plan detail's branch tabs). (Contents becomes read-write in Phase 2.) Adding Pull requests to an existing App makes each installation owner re-approve; until they do, the board degrades to a "grant access" notice instead of erroring.
- Subscribe to events:
Push,Pull request,Installation,Installation repositories. - Enable "Request user authorization (OAuth) during installation" / generate a client secret so user login works.
- Generate a private key (downloads a
.pem).
Then Install the App on your account or an org.
npx wrangler d1 create plans
# Copy the printed database_id into wrangler.jsonc (replace REPLACE_WITH_D1_DATABASE_ID)
npm run db:migrate:remote # apply migrations to the remote D1Local dev: copy .dev.vars.example to .dev.vars and fill it in.
Production: set each as a Worker secret:
for s in GITHUB_APP_ID GITHUB_APP_CLIENT_ID GITHUB_APP_CLIENT_SECRET \
GITHUB_APP_PRIVATE_KEY GITHUB_WEBHOOK_SECRET SESSION_SECRET \
TOKEN_ENCRYPTION_KEY APP_URL; do npx wrangler secret put "$s"; doneSESSION_SECRET—openssl rand -base64 48TOKEN_ENCRYPTION_KEY—openssl rand -base64 32(must decode to 32 bytes)GITHUB_APP_PRIVATE_KEY— paste the full.pem(PKCS#1 or PKCS#8 both work)APP_URL— the public origin, no trailing slash (https://plans.superhighfives.com)
npm install
npm run db:migrate:local # apply migrations to local D1
npm run dev # http://localhost:5173Exercising the "move to done" verify flow locally requires a container
runtime (Docker) — wrangler dev uses it to run the Sandbox container that
clones, installs, and runs a repo's test/build scripts.
Pushes to main deploy automatically via GitHub Actions
(.github/workflows/deploy.yml). The workflow waits for CI to pass on the
commit, then applies pending D1 migrations (--remote) and runs
wrangler deploy.
For CI deploys to work, add two repository secrets (Settings → Secrets and variables → Actions):
CLOUDFLARE_API_TOKEN— scoped token with Workers Scripts: Edit and D1: Edit permissions.CLOUDFLARE_ACCOUNT_ID— your Cloudflare account ID.
The Worker secrets from step 3 and the D1 database_id are set once (as above)
and persist across deploys — CI never touches them.
Production is served at https://plans.superhighfives.com via the
custom-domain route in wrangler.jsonc. Cloudflare provisions the DNS record
and TLS cert on the first deploy, so the superhighfives.com zone must be on
the same Cloudflare account.
To deploy by hand instead (e.g. the very first deploy, before the workflow is on
main):
npm run deploy # build + wrangler deploy| Script | Purpose |
|---|---|
npm run dev |
Local dev server (Vite + Workers runtime) |
npm run build |
Production build (also regenerates the route tree) |
npm run deploy |
Build and deploy to Cloudflare (manual; CI deploys on push to main) |
npm run typecheck |
tsc --noEmit |
npm test |
Unit tests (frontmatter, plan paths, skill sync, crypto) |
npm run db:generate |
Generate a D1 migration from the schema |
npm run db:migrate:local / :remote |
Apply migrations |
- Every server function that touches private data runs
authMiddleware; the RPC endpoint — not the route — is the auth boundary. - Repo/plan access is re-checked against the user→installation mapping on every request, so one tenant can't read another's repos by guessing owner/name.
- Installation tokens are cached encrypted (AES-256-GCM) in D1.
- Session and OAuth-state cookies are HttpOnly, SameSite=Lax, and use the
__Host-prefix + Secure in production; OAuth uses a signedstatecookie (GitHub OAuth doesn't support PKCE). - The webhook verifies
X-Hub-Signature-256before trusting any payload.
These are deliberate simplifications for the first shippable slice; each has a clear upgrade path noted in the code:
- Discovery runs inline, not on Queues. The plan calls for Cloudflare Queues
fan-out. v1 runs the scan on-demand (first dashboard load + manual "Rescan"),
with bounded concurrency.
scanInstallation/scanRepoare factored so they can be lifted into a queue consumer without change. - Sessions are stateless signed cookies (no server-side session table). The layer is intentionally thin so an identity provider (or DB sessions) can be slotted in later.
- The user OAuth token is not persisted. It's used only during the callback to identify the user and snapshot their installations. Installing the App somewhere new later is picked up by signing in again.
- Discovery does a lightweight presence check (a
plans/<state>/*.mdfile exists); full frontmatter validation happens when a repo/plan is opened, and invalid plans are skipped there.
Private / unreleased.