ImaGen #7: cloud-sync (Supabase Storage + imagen.images schema) for the flexsiebels viewer #7
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
After every successful
imagen generate, also upload the PNG to a Supabase Storage bucket and insert a row into a newimagen.imagesschema. This is the data plane that the flexsiebels owner-mode viewer (separate issue onm/flexsiebels.de, coordinated by paul) reads from.Supersedes the standalone-viewer option D from ImaGen#6. Joint plan negotiated head-to-head with paul (flexsiebels/head) on 2026-05-10 — m confirmed owner-only v1 with a future promotion-path to
flexsiebels.imagesas the publish action.Why Supabase, not a local mRiver static site
/imagineroutes there matches the existingmb()/flex()/mh()schema-helper pattern (see flexsiebels CLAUDE.md). No new infra.Scope
1. Schema migration
Apply via
mcp__supabase__apply_migrationto the dev Supabase. Sketch from paul (basically verbatim):Leave
mai.imagen_usageuntouched — different concern (cost-billing, prompt-hash-only). Join onprompt_hashwhen the viewer wants to surface cost telemetry.Row-level security: enable RLS on
imagen.imageswith a policy that allows the owning user (owner_user_id = auth.uid()) read/insert. flexsiebels' owner-mode is single-user (m), but the policy is the contract.2. Storage bucket
Create a private Supabase Storage bucket
imagen-generated. Path convention:<YYYY-MM-DD>/<slug>-<seed>.png. Access via signed URLs only (no public reads).3. Go writer changes
New package
internal/cloud/or extendinternal/output/:mai.imagen_usagecost-tracking —internal/usage/usage.go). Addimagen_user_idto config (a UUID config key; m's auth.users id) — sample default value commented but unfilled in the template.imagen-generated/<date>/<slug>-<seed>.png(use the same date-slug-seed convention as the local filename).imagen.imageswith all metadata derived from the sidecar (prompt, prompt_hash via sha256, backend, model, seed, steps, width, height, latency_ms, cost_usd_estimate, storage_path, full sidecar JSON, empty tags).imagen: cloud sync: <err>), exit code stays 0. Don't fail the user's render over a cloud blip.4. New
--no-cloudflag onimagen generateFor offline / private / sensitive generations. Suppresses both the upload and the DB insert. Add a config knob
output.cloud_sync: auto|on|offmirroring the--previewpattern from #5.5. Owner-UUID resolution
New config field
owner_user_id: <UUID>in~/.config/imagen.yaml.imagen config initwrites it as an empty placeholder with a comment pointing at the Supabase auth.users table.imagen config validatewarns if cloud-sync is enabled butowner_user_idis unset.For head's smoke test, look up m's UUID via
mcp__supabase__execute_sql(SELECT id FROM auth.users WHERE email = ...) and write it into the config before running.6. Tests
internal/cloud/cloud_test.go: mocked Storage + DB client (httptest server), happy path + retry-on-5xx + 4xx-no-retry + missing-owner-uuid clean error.7. Smoke test
One real generation against mRock with cloud-sync on:
Report DONE with the resulting
imagen.images.idand a signed URL.Acceptance criteria
imagen.imagestable exists in dev Supabase with the schema above, RLS policy in place, indexes created.imagen-generatedprivate bucket exists in Supabase Storage.imagen generate "..."(without--no-cloud) writes the local PNG/sidecar AND uploads to the bucket AND inserts intoimagen.images. All three.imagen generate "..." --no-cloudonly writes locally; no Storage upload, no DB row.imagen config initwrites the newowner_user_id+output.cloud_syncfields;imagen config validatewarns on missing owner UUID.go build ./... && go test ./...clean.Out of scope
POST /generateHTTP endpoint to launch generations from flexsiebels — deferred to a v2 issue once m wants UI-triggered generation.imagen.images→flexsiebels.images) — separate future issue once owner-only v1 is live.tags TEXT[]column exists; tagging surface lives in the flexsiebels viewer.Refs
mb()/flex()/mh()); RLS pattern; owner-mode auth boundary.mai.imagen_usagecost-tracking precedent (also Supabase-write; reuse the auth/client pattern).Workflow
Coder role. Branch yours via
mai hireworktree. End shift withmake build+go test ./...clean + the one real-API smoke test from §7. Head reviews + merges into main + comments commit link on this issue + appliesdonelabel.shift-1 done. Branch
mai/hermes/issue-7-imagen-7-cloud, commite22f286.What landed
Schema + infra (msupabase): 4 applied migrations.
imagen_schema_init—imagen.imagestable per the §1 contract (UUID PK +auth.users(id)FK, prompt +prompt_hash+ backend/model/seed/steps/width/height/latency_ms/cost_usd_estimate/storage_path + sidecar JSONB +tags TEXT[]); three indexes (owner+created_at DESC, backend, gin(tags)); RLS enabled withowner_user_id = auth.uid()SELECT + INSERT policies.imagen_schema_grants— USAGE to authenticated/service_role/anon; full DML onimagen.imagesto authenticated + service_role; default privileges set.imagen-generated— private, 50MB limit, png/jpeg/webp.imagen_storage_policies—storage.objectsSELECT policy joins back toimagen.images.storage_pathso an authenticated owner can read their own files via signed URLs (and only theirs); INSERT policy gates on bucket_id.imagen_pgrst_expose—ALTER ROLE authenticator SET pgrst.db_schemasto addimagento the existing list, thenNOTIFY pgrst, 'reload config'. (FYI: the role-level setting is the source of truth on msupabase, not the container env file. Touched the .env briefly during diagnosis, reverted.)Code: new
internal/cloud/package mirroringinternal/usage/shape — PostgREST POST against theimagenschema (Accept-Profile/Content-Profileheaders), Storage upload via PUT withx-upsert: true, retry on 5xx + transport but not 4xx,owner_user_idrequired (the column is NOT NULL and the read-side RLS policy needs it).cmd/imagen/generate.gogot the--no-cloudflag,output.cloud_syncconfig knob (auto|on|off mirroring--preview), and$IMAGEN_CLOUD_SYNCenv override. The hook reads the just-written PNG + sidecar from disk and callscloud.Sync; failures emitimagen: cloud sync: <err>to stderr without changing exit code.output.OutputsgrewDate/Slug/Seedfields sostorage_pathmirrors the local filename's prefix exactly (no UTC-vs-local drift).Config:
owner_user_idfield added toConfig; sample comment points at theauth.userslookup.imagen config validatewarns on stderr whencloud_syncison/autobutowner_user_idis empty. m's UUID isac6c9501-3757-4a6d-8b97-2cff4288382b(paragraphenreiter@gmail.com — only m account inauth.users).Tests:
cloud_test.gocovers happy path, retry-on-5xx, no-retry-on-4xx, missing-owner-uuid, missing-date-or-slug, signed URL, and the partial-success case (upload landed, DB insert failed).generate_test.gocovers the precedence chain for cloud-sync mode resolution.go build ./... && go test ./...clean across the tree.Smoke test (§7)
DB row:
0b8e7560-aa10-4493-bf42-6c0c536e8cbf2026-05-11/a-tiny-lighthouse-on-a-stormy-cliff-phot-11.pngSigned URL (24h, regenerable on demand): https://supa.flexsiebels.de/storage/v1/object/sign/imagen-generated/2026-05-11/a-tiny-lighthouse-on-a-stormy-cliff-phot-11.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJpbWFnZW4tZ2VuZXJhdGVkLzIwMjYtMDUtMTEvYS10aW55LWxpZ2h0aG91c2Utb24tYS1zdG9ybXktY2xpZmYtcGhvdC0xMS5wbmciLCJpYXQiOjE3Nzg0NTcwNzksImV4cCI6MTc3ODU0MzQ3OX0.wYXw1XvhzRE8GVoNrbMmRsrfHuxmFY_vCHUwuK7CyFo
Round-trip verified:
curl→ 200, 1394068 bytes,image/png, byte-identical to the local file.--no-cloudverified to skip both Storage and DB (seed=99 generation produced local files only,count(*) WHERE seed=99is 0 inimagen.images).Acceptance
imagen.imagesexists with the spec'd schema, RLS, indexes.imagen-generatedbucket exists, private, 50MB cap.imagen generatewrites local PNG + sidecar AND uploads AND inserts.--no-cloudwrites locally only.owner_user_id+output.cloud_syncin sample config;validatewarns on missing owner UUID.go build && go testclean.Commit:
e22f2860c7Branch: https://mgit.msbls.de/m/ImaGen/src/branch/mai/hermes/issue-7-imagen-7-cloud
Setting
needs-review.Merged into main
Branch
mai/hermes/issue-7-imagen-7-cloudmerged via--no-ff. Pushed to origin/main.e22f286Acceptance criteria
imagen.imagesschema exists in msupabase with RLS + indexesimagen_schema_init,imagen_schema_grants,imagen_storage_policies, plus the PGRSTdb_schemasruntime update + NOTIFY reloadimagen-generatedprivate Storage bucket existsimagen generate "..."writes local + uploads + DB-inserts (all three)imagen.images.id=0b8e7560-aa10-4493-bf42-6c0c536e8cbffrom prompta tiny lighthouse on a stormy cliff, photo; bucket object2026-05-11/a-tiny-lighthouse-on-a-stormy-cliff-phot-11.png--no-cloudonly writes locallydoRetry/doRetryReadhelpers, mocked-HTTP tests cover 5xx-retry / 4xx-no-retryimagen config initwritesowner_user_id+output.cloud_syncimagen config validatewarns on missing UUID when cloud-sync is ongo build ./... && go test ./...cleaninternal/cloud/cloud_test.gosuite, all packages passEnd-to-end with flexsiebels (verified by paul)
fexsiebels.de#64 (paul, knuth) merged at
6c1583f. paul ran a manual data-plane check againstimagen.images.id=0b8e7560-...:2026-05-11/a-tiny-lighthouse-on-a-stormy-cliff-phot-11.pngThe
imagen() -> .schema('imagen').select -> createSignedUrl()pattern is proven on data.One open infra ticket (not blocking)
Dokploy auto-deploy didn't fire on flexsiebels' #64 merge (same gap dokploy-worker flagged for fdbck last week). The flexsiebels container is still on the old image, so https://flexsiebels.de/imagine returns 404 until the redeploy lands. paul nudged the dokploy worker to (a) trigger manual redeploy now, (b) optionally fix the Gitea -> Dokploy webhook for both apps. Once the deploy lands, paul runs the visual smoke. No imagen-side action needed.
Owner UUID for the record
m's
auth.users.idisac6c9501-3757-4a6d-8b97-2cff4288382b(paragraphenreiter@gmail.com — the only m account). Now the documented default inimagen config init's template.Cross-link
mai.imagen_usagecost-tracking schema. Still separate. Join onprompt_hashwhen needed.