feat(integration): Day 5 — Supabase agent tools + sandbox env injection#5
Conversation
Agents can now auto-provision a Supabase project under the user's
connected org and run DDL/RLS/seed SQL against it via Management API +
OAuth Bearer (no service-role key needed). The platform mirrors the
active project's URL + anon key into the sandbox's .env.local on every
acquire, so generated apps read VITE_SUPABASE_URL / VITE_SUPABASE_ANON_KEY
straight from import.meta.env.
- supabase_create_project tool: idempotent provisioning, polls until
ACTIVE_HEALTHY (~30-60s, 90s timeout), persists ref/url/anon_key on
the conversation row, writes .env.local to current sandbox
- supabase_sql tool: runs SQL via /v1/projects/{ref}/database/query,
with 100-row truncation
- supabase-client: createSupabaseProject / getSupabaseProjectKeys /
runSupabaseSql / listSupabaseOrganizations + 3 typed errors
- conversation-sandbox: re-writes .env.local on every acquire so it
stays in sync with the conversation row (covers reconnect/rotate/
disconnect transparently)
- file-tracker: ignores .env.local* so platform-managed env doesn't
pollute fileSnapshots
- prompts: PROJECT_CONVENTIONS gains a Supabase chapter (when to call
supabase_create_project, RLS + auth.uid() defaults, never expose
service-role)
- schema: conversations += supabase_url + supabase_anon_key
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… a skill
Per review feedback: Supabase usage is a per-project concern, not a
universal one — every conversation paying tokens for it (even pure
static-site builds) is wasteful. Move the full guide into a sandbox
skill so the agent only loads it on relevant triggers.
- New skill at sandbox-template/skills/supabase/SKILL.md
(mirrors hono-fullstack's structure)
- Workflow leads with "ALWAYS supabase_create_project first
(idempotent)" — agent doesn't need to track provisioning state
- Concrete recipes: RLS policies, auth signUp/signIn, file storage
bucket creation, realtime channel + ALTER PUBLICATION
- Don'ts cover the platform contract (no .env.local writes, no
service-role attempts, no skipping RLS)
- PROJECT_CONVENTIONS shrinks the 2-paragraph chapter back to a single
pointer line: "When the app needs persistent data ... use the
supabase skill"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Addressed review point 1 in Why this matters: every conversation was paying prompt tokens for the 2-paragraph Supabase block, even when building static sites that don't touch Supabase. Skills are loaded on-demand by description triggers — pure-static conversations now pay zero. Skill design highlights:
Still on the table for follow-up (review point 2 was deferred):
Note: the skill source ships in |
6337682 to
cc3cb06
Compare
…on connection - Rename supabase-app → frontend-starter; the template is now a neutral Vite + React + TanStack starter with no Supabase coupling (drop @supabase/supabase-js, supabase.ts, use-auth.ts, auth demo route). - Persistence is conditional and decided server-side: when the user has connected Supabase, the agent run includes supabase_create_project + supabase_sql tools and the supabase skill. When not connected, those tools are removed from the run and a <persistence> system reminder tells the agent to use in-memory state (not localStorage) and inform the user that data won't persist beyond the preview tab. - Auto-confirm signups (mailer_autoconfirm=true) at project provision — preview sandboxes can't honor the default email-redirect Site URL, so demos use email/password without an email-link round trip. - hono-fullstack SKILL.md cross-refs updated; iframe-runtime vendor target follows the rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…on env.parse CI runs without .env, but env.ts validates required strings (DATABASE_URL, SUPABASE_URL, LLM_API_KEY, …) at module load. Any test transitively importing env (via crypto.ts → env.ts) crashed during test collection. Set placeholder defaults for all required env vars after dotenv load — local .env values still win, CI falls back to harmless test placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Day 5 of the BYO publish + DB plan. After Day 4 wired Supabase OAuth, this PR teaches the agent to actually use the connection — auto-provision a project under the user's org, run schema/RLS/seed SQL against it, and have the generated Vite app read the project URL + anon key straight out of
import.meta.env.supabase_create_projecttool — idempotent provisioning. Calls Management APIPOST /v1/projects, pollsGET /v1/projects/{ref}untilACTIVE_HEALTHY(~30–60s, 90s timeout), persistssupabase_project_ref+supabase_url+supabase_anon_keyon the conversation row, writes.env.localto the running sandbox. If a project already exists for the conversation, returns the existing one and just refreshes.env.local.supabase_sqltool — runs SQL viaPOST /v1/projects/{ref}/database/queryunder the user's OAuth Bearer; no service-role key needed. 100-row truncation on returns.supabase-client.ts— addscreateSupabaseProject/getSupabaseProject/getSupabaseProjectKeys/runSupabaseSql/listSupabaseOrganizations+ 3 typed errors (SupabaseManagementApiError,SupabaseProjectProvisionTimeoutError,SupabaseProjectInitFailedError). All gated throughgetValidSupabaseAccessToken(userId).acquireConversationSandbox— re-writes.env.localon every acquire from the conversation row so disconnect/rotate/reconnect propagates automatically. Cold starts (sandbox evicted, restart from snapshots) get.env.localback without an agent turn.file-tracker— ignores.env.local+.env.*.localso platform-managed env doesn't pollutefileSnapshots(and therefore doesn't get re-restored on cold start, which would race the new injection).PROJECT_CONVENTIONSgains a Supabase chapter: when to callsupabase_create_project(persistence/auth/storage/realtime only),import.meta.env.VITE_SUPABASE_*is platform-injected (don't write.env.localyourself), alwaysENABLE ROW LEVEL SECURITY+auth.uid()policies, service-role key is not exposed.conversations+=supabase_url+supabase_anon_key(nullable text). Applied viadb:push.Architectural notes
/v1/projects/{ref}/database/queryis enough. Avoids the security footgun of pulling service-role keys into the platform / sandbox.supabase_url+supabase_anon_keyon the conversation row:acquireConversationSandboxruns on every cold start and reload; we don't want a Management API round-trip there. Anon key is public-by-design (RLS is the security boundary), so persisting it next to the project ref is fine..env.localin file-tracker: if it landed infileSnapshots, cold-start would restore the snapshot version while T7's injection writes the current version — order race + stale env. Cleaner to make the platform the sole writer.Test plan
bunx tsc --noEmit✅, run locally to confirm)bun run test— 36/36 pass locally)supabase_create_project→ ~30–60s wait → returns ref + URLsupabase_sqltoCREATE TABLE+ENABLE ROW LEVEL SECURITY+CREATE POLICY ... auth.uid() = user_idsupabase_create_projecta second time on the same conversation → returns the existing ref, no new project createdsupabase_create_project→ tool returns the typed error message asking the user to reconnect (not a cryptic 401)supabase_project_ref→.env.localis re-written before bootstrap fires.env.localdoes NOT appear infileSnapshotsfor any test conversationOut of scope (planned for later)
@hono/vercel+ build-time env injection (.env.production)🤖 Generated with Claude Code