A forkable template that lets you fire build tasks at a project from a Claude chat or any phone, have a worker on a server do the work, and approve the results — without depending on a specific device.
Fork it once into a GitHub template repo; every future project is then one
create-repo task away.
Claude (any chat) Vercel (front door) Supabase
fire by curl + bearer ─────▶ /api/tasks ──────────────▶ Postgres queue
console (/status/remote) + Google auth
You (any phone) ─────▶ Google sign-in ─────────────▶ (RLS allowlist)
▲
outbound poll │
Hetzner box · worker.py ────────────┘
(no inbound surface · holds secrets)
Why this shape:
- The box has no open ports.
runner/worker.pypolls Supabase outbound for queued tasks and runs them. Nothing to expose, nothing to IP-block. - The front door is Vercel — a single
/api/tasksroute. Claude authenticates with a static agent bearer token; you authenticate with Google sign-in (Supabase Auth), locked to an email allowlist. - Secrets live in exactly two places: the box
.envand Vercel env. Never in the browser, never in a chat, never in a doc.
This is the one deliberate deviation from docs/TF-AUTONOMY-PLAN.md: the API
moved off the box (no more on-box FastAPI) because Supabase + Vercel do that job
better and leave the box dark. See docs/GPRED-LEARNINGS.md for what carried over
from the first Hetzner loop and what didn't.
taskify/
├─ README.md ← this file
├─ LICENSE ← MIT
├─ .gitignore ← protects .env, node_modules, venv, logs/, repos/, .next
├─ .env.example ← the secret contract (box + Vercel + console)
├─ runner/ ← the box worker (no inbound surface)
│ ├─ worker.py ← poll Supabase → dispatch → write status back
│ ├─ handlers.sh ← one handler per task type (build, claude-code, …)
│ ├─ bootstrap.sh ← one-shot box setup: venv, deps, systemd service
│ ├─ schema.sql ← Supabase tables, RLS, allowlist
│ └─ curl.sh ← the fire / poll recipe Claude runs from a chat
├─ web/ ← the Vercel front door + console (Next.js App Router)
│ ├─ app/api/tasks/route.ts ← the single front door (agent bearer + Google)
│ ├─ app/status/remote/ ← the remote-control console (dark)
│ ├─ app/styles/tf-tokens.css ← TasteForge design tokens, vendored verbatim
│ └─ public/setup.html ← the hostable setup wizard
└─ docs/
├─ TF-AUTONOMY-PLAN.md ← tiers, milestones, the gate matrix + design rubric
├─ TF-REMOTE-CONTROL.md ← the paste-into-any-chat remote-control charter
└─ GPRED-LEARNINGS.md ← reusable patterns from the first Hetzner loop
You touch a terminal once (steps 2–3, from a laptop). Everything after is
chat-and-phone. A hostable walkthrough lives at /setup.html once web/ is
deployed.
- Create a project. Open the SQL editor and run
runner/schema.sql. - Add your email:
insert into allowed_users(email) values ('you@tasteforge.ai'); - Seed a project:
insert into projects(id,name,repo) values ('app-skeleton','App skeleton','app-skeleton'); - (Optional now) Authentication → Providers → enable Google, set the redirect to the Supabase callback shown on that screen.
scp -r runner/ .env.example you@your-box:/opt/tf/ # worker, handlers, bootstrap
ssh you@your-box
cd /opt/tf && cp .env.example .env && nano .env # fill in the box secrets
bash bootstrap.sh # venv, deps, systemd service
journalctl -u tf-runner -f # watch it pollThe service restarts on crash and on reboot, and polls Supabase for queued tasks.
- Import
web/as a Vercel project (root directoryweb). - Set the Vercel env vars from
.env.example(the front-door + console blocks).SUPABASE_SERVICE_KEYandAGENT_TOKENare server-only — neverNEXT_PUBLIC_. - Deploy. Your endpoint is
https://<app>.vercel.app/api/tasksand the console is athttps://<app>.vercel.app/status/remote.
Paste docs/TF-REMOTE-CONTROL.md into a new Claude chat, then give it the
endpoint URL and the AGENT_TOKEN for that session. Claude runs curl.sh-style
calls. The console’s live mode talks to the same /api/tasks over your signed-in
Google session.
| Caller | Auth | Path |
|---|---|---|
| Claude (chat) | AGENT_TOKEN bearer |
curl → /api/tasks |
| You (any phone) | Google sign-in (Supabase) | console → /api/tasks |
| The worker | Postgres service connection | outbound poll, no inbound |
Autonomous: build · typecheck · preview · screenshot · claude-code
on a feature branch · design-eval · ingest to a scratch branch ·
backlog-pull · create-repo.
Ask first (held for your approval): merge to main, production data, anything
a client sees, schema migrations, major dependency bumps, deletions.
Never (hard stop): push to main without review; read/write/echo a secret;
expose an engine codename on an external surface; irreversible deletion; act
outside the repo + allowed infra; disable a gate.
Frontend-producing tasks resolve to needs-review, never succeeded — a
human approves the surface. Every frontend task carries a design-eval step and
is scored against the 7-axis rubric in docs/TF-AUTONOMY-PLAN.md; nothing below
the bar surfaces for review. runner/worker.py enforces the review gate
(REVIEW_TYPES), and the console mirrors it.
Custom interfaces follow the
tasteforge design kit as
the single source of truth: Inter, the locked tokens, JetBrains Mono on numerics,
European sentence case, no emoji, light theme by default. The kit’s
@tasteforge/ui package is an unpublished workspace gated behind Untitled UI PRO,
so — per the kit’s own recommendation — this template vendors its token + theme
CSS verbatim at web/app/styles/ (tf-tokens.css, tf-themes.css). Re-point
those imports at @tasteforge/ui/tokens.css once the package is published.
The one sanctioned dark surface is the technical remote-control console at
/status/remote; everything else is light. (JetBrains Mono is added in
tf-mono.css because the kit ships no mono token yet but the system mandates mono
numerics.)
cd web
npm ci
npm run dev # http://localhost:3000 → /status/remote runs in demo mode
npm run build # production build (green with no secrets present)The console boots in demo mode (a simulated loop in the browser); switch to
live from the connection panel to talk to a deployed /api/tasks.
The master control room is just the console reading every project row in the one shared Supabase. To spin up a new project on the same loop:
- Make this repo a GitHub template (Settings → Template repository).
- Set
GITHUB_TEMPLATEin the box.envto this repo’s name, andGITHUB_ORGto your org. - Fire a
create-repotask from a chat or the console:The worker calls the GitHub generate-from-template API (curl -sS -X POST "$API" -H "$AUTH" -H 'content-type: application/json' \ -d '{"type":"create-repo","payload":{"name":"my-next-thing"}}'
handlers.sh→create-repo) to create a new private repo from the template under your org. - Register it so the console picks it up:
insert into projects(id,name,repo) values ('my-next-thing','My next thing','my-next-thing');
- New project, same loop, same console — no new infra. The box clones the repo on first task and works only on feature branches; you keep the merge and design gates.
Security: the GitHub PAT is fine-grained, scoped to the org’s repos, contents + (for
create-repo) administration, and lives only in the box.env.SUPABASE_SERVICE_KEYbypasses RLS and is server-side only — the browser uses the anon key, and RLS + the allowlist protect the data. RotateAGENT_TOKENwhenever you like.