From 27bf61cea88600251eef666c2963f8bd1cca4f1e Mon Sep 17 00:00:00 2001 From: Babayaga Date: Wed, 8 Apr 2026 11:45:33 +0200 Subject: [PATCH] supabase --- packages/ui/docs/supabase-ditch-images.md | 202 ++++++++++++++++++++++ packages/ui/src/lib/uploadUtils.ts | 27 ++- 2 files changed, 215 insertions(+), 14 deletions(-) create mode 100644 packages/ui/docs/supabase-ditch-images.md diff --git a/packages/ui/docs/supabase-ditch-images.md b/packages/ui/docs/supabase-ditch-images.md new file mode 100644 index 00000000..8737a28a --- /dev/null +++ b/packages/ui/docs/supabase-ditch-images.md @@ -0,0 +1,202 @@ +# Supabase -> Internal VFS (Images) Trial Plan + +## Goal + +Move **new image uploads** from Supabase Storage to internal VFS mount configured by `IMAGE_VFS_STORE` (trial default: `images`), while keeping: + +- existing `pictures` table workflow (`createPictureRecord` with meta) +- ability to read old Supabase URLs +- fast rollback to Supabase write path + +This is a **write-path migration first**, not a full historical backfill cutover. + +--- + +## Current State (verified) + +- Frontend upload goes to `POST /api/images?forward=supabase&original=true` from `src/lib/uploadUtils.ts`. +- Server image endpoint handles `forward === 'supabase'` in `server/src/products/images/index.ts`. +- VFS mount already exists: + - mount: `images` + - path: `./storage/images` + - config: `server/config/vfs.json` +- VFS API exists for file reads/uploads under `server/src/products/storage/api/vfs.ts`. + +--- + +## Constraints / Requirements + +- Keep `pictures` insert behavior intact (title/meta/etc). +- New `image_url` should be VFS-readable URL (mount=`IMAGE_VFS_STORE`, path inside user folder). +- Rollback must be immediate and low risk. +- Trial should be controllable without broad code churn. + +--- + +## URL Strategy (important) + +Use a stable public URL shape for VFS-hosted images: + +- `image_url = /api/vfs/get///` + +Notes: + +- This keeps `image_url` directly consumable by clients. +- It avoids exposing filesystem paths. +- It maps cleanly to VFS mount + subpath permissions. +- Existing Supabase URLs remain untouched for historical records. +- `token` query param is optional at request time for protected reads, but it is **not stored** in `pictures.image_url`. + +`IMAGE_VFS_URL` is the canonical server base for these URLs. + +--- + +## Phased Execution + +## Phase 0 - Safety Rails (no behavior change) + +1. Add env-driven upload target on server: + - `IMAGE_UPLOAD_TARGET=supabase|vfs` (default `supabase`) + - `IMAGE_VFS_STORE=images` (trial default mount name) +2. Keep query param override support: + - `forward` query still accepted (`supabase`/`vfs`) + - precedence: explicit query param -> env default +3. Add structured logs: + - chosen target, user id, filename/hash, bytes, duration, status + +Rollback: N/A (no functional switch yet). + +## Phase 1 - Add `forward=vfs` in image endpoint + +In `server/src/products/images/index.ts` (`handlePostImage`): + +1. Reuse already-produced `processedBuffer` and `filename`. +2. Implement `if (forward === 'vfs')` branch: + - Determine user id from auth context (same identity used today for inserts). + - Build relative VFS path: `/`. + - Resolve mount from `IMAGE_VFS_STORE` (default `images`). + - Write bytes to resolved mount destination (for `images`: `./storage/images//`), either: + - through internal VFS write/upload helper (preferred), or + - direct fs write to resolved mount path if helper coupling is expensive. +3. Return JSON same shape as Supabase branch: + - `url`: `/api/vfs/get///` + - include width/height/format/size/meta as now. + +Safety: + +- Keep current Supabase branch unchanged. +- If VFS write fails and trial fallback is enabled, optionally retry Supabase write. + +## Phase 2 - Frontend trial toggle (small change) + +In `src/lib/uploadUtils.ts`: + +1. Replace hardcoded `forward=supabase` with configurable target: + - `VITE_IMAGE_UPLOAD_FORWARD` (`supabase` default, `vfs` for trial users/environments) +2. Keep response handling unchanged (`{ url, meta }`). +3. Keep `createPictureRecord` unchanged except optional `type` marker (see below). + +Recommended optional marker: + +- set `type` to `vfs-image` or `supabase-image` for observability and easy SQL filtering. + +## Phase 3 - Trial rollout + +1. Enable `vfs` only in selected environment(s) or for selected users. +2. Monitor: + - upload success/failure rate + - read success for generated `image_url` + - latency vs Supabase path + - disk growth under `server/storage/images` +3. Validate ACL behavior: + - only expected users can access private paths (or keep public semantics intentionally). + +## Phase 4 - Decide on default + +If trial is healthy: + +- change default upload target to `vfs` +- keep Supabase path available for emergency rollback for at least one release cycle + +--- + +## Data Model / Record Semantics + +No schema migration required for trial. + +- Keep writing `pictures` row as-is. +- `image_url` stores either: + - old Supabase public URL (historical rows), or + - new VFS URL (`/api/vfs/get//...`) for new rows. + +Strongly recommended: + +- include `storage_backend` in `meta` (`"supabase"` or `"vfs"`), or use `type` column consistently. + +This enables mixed-backend rendering and easier operational debugging. + +--- + +## Rollback Plan (must be instant) + +Primary rollback switch: + +1. Set `IMAGE_UPLOAD_TARGET=supabase` on server. +2. Keep `IMAGE_VFS_STORE=images` as-is (or any mount name; ignored while on supabase target). +3. Set `VITE_IMAGE_UPLOAD_FORWARD=supabase` on frontend (if used). +4. Redeploy/restart. + +Behavior after rollback: + +- New uploads go to Supabase again. +- Existing VFS-backed records continue to load via VFS read endpoint. +- No data loss, no record rewrite needed. + +Emergency fallback option: + +- In server `forward=vfs` branch, on write failure, fallback to Supabase upload and log fallback event. + +--- + +## Test Plan + +## Unit / integration + +- `forward=supabase` still returns 200 and public Supabase URL. +- `forward=vfs` returns 200 and `/api/vfs/get///`. +- VFS write failure path returns clear error (or controlled fallback). +- metadata extraction remains present in response for both paths. + +## E2E manual + +1. Upload image with `forward=vfs`. +2. Confirm file exists in `server/storage/images//`. +3. Open returned `image_url`; verify content-type and caching headers. + - verify base URL works directly; for protected ACL mode, verify request-time `?token=...` also works. +4. Confirm `pictures.image_url` and `meta/type` are correct. +5. Switch env back to Supabase and repeat to verify rollback. + +--- + +## Open Decisions + +1. **Auth model for VFS read URL** + - public read (Supabase-like behavior) vs authenticated read +2. **Canonical URL** + - keep absolute via `IMAGE_VFS_URL` (recommended and now selected) +3. **Collision policy** + - current hash-based filename is deterministic; acceptable to overwrite same hash +4. **Backfill** + - not needed for trial, but define later if we want full Supabase deprecation + +--- + +## Minimal Implementation Checklist + +- [ ] Add `forward === 'vfs'` branch in image upload endpoint. +- [ ] Add server env defaults (`IMAGE_UPLOAD_TARGET`, `IMAGE_VFS_STORE='images'`). +- [ ] Add frontend env toggle (`VITE_IMAGE_UPLOAD_FORWARD`) instead of hardcoded `supabase`. +- [ ] Persist backend marker (`meta.storage_backend` and/or `type`). +- [ ] Add metrics/logging for backend choice and failures. +- [ ] Run trial in one environment/user cohort. +- [ ] Keep rollback toggles documented in runbook. diff --git a/packages/ui/src/lib/uploadUtils.ts b/packages/ui/src/lib/uploadUtils.ts index 446e81d8..58b8ac25 100644 --- a/packages/ui/src/lib/uploadUtils.ts +++ b/packages/ui/src/lib/uploadUtils.ts @@ -1,28 +1,27 @@ import { supabase } from '@/integrations/supabase/client'; +import { getAuthToken, serverUrl } from './db'; + +const IMAGE_UPLOAD_FORWARD_PRESET = (import.meta.env.VITE_IMAGE_UPLOAD_FORWARD || 'vfs').toLowerCase() === 'supabase' + ? 'supabase' + : 'vfs'; /** - * Uploads an image file via the server API (sharp processing + Supabase forwarding). - * Always goes through VITE_SERVER_IMAGE_API_URL/api/images. + * Uploads an image file via the server API. + * Call sites are storage-agnostic; this module enforces the internal upload target preset. */ export const uploadImage = async (file: File, userId: string): Promise<{ publicUrl: string, meta?: any }> => { if (!userId) throw new Error('User ID is required for upload'); - - const SERVER_URL = import.meta.env.VITE_SERVER_IMAGE_API_URL; - if (!SERVER_URL) throw new Error('VITE_SERVER_IMAGE_API_URL is not configured'); - - // Get auth token for authenticated upload - const { data: { session } } = await supabase.auth.getSession(); - - console.log(`Uploading ${file.name} (${file.size} bytes) via server API...`); + if (!serverUrl) throw new Error('VITE_SERVER_IMAGE_API_URL is not configured'); + const token = await getAuthToken(); const formData = new FormData(); formData.append('file', file); const headers: Record = {}; - if (session?.access_token) { - headers['Authorization'] = `Bearer ${session.access_token}`; + if (token) { + headers['Authorization'] = `Bearer ${token}`; } - const response = await fetch(`${SERVER_URL}/api/images?forward=supabase&original=true`, { + const response = await fetch(`${serverUrl}/api/images?forward=${IMAGE_UPLOAD_FORWARD_PRESET}&original=true`, { method: 'POST', headers, body: formData, @@ -58,7 +57,7 @@ export const createPictureRecord = async (userId: string, file: File, publicUrl: title: file.name.split('.')[0] || 'Uploaded Image', description: null, image_url: publicUrl, - type: 'supabase-image', + type: IMAGE_UPLOAD_FORWARD_PRESET === 'vfs' ? 'vfs-image' : 'supabase-image', meta: meta || {}, }) .select()