supabase
This commit is contained in:
parent
07d1341c76
commit
27bf61cea8
202
packages/ui/docs/supabase-ditch-images.md
Normal file
202
packages/ui/docs/supabase-ditch-images.md
Normal file
@ -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 = <IMAGE_VFS_URL>/api/vfs/get/<IMAGE_VFS_STORE>/<user_id>/<filename>`
|
||||
|
||||
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: `<user_id>/<filename>`.
|
||||
- Resolve mount from `IMAGE_VFS_STORE` (default `images`).
|
||||
- Write bytes to resolved mount destination (for `images`: `./storage/images/<user_id>/<filename>`), 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`: `<IMAGE_VFS_URL>/api/vfs/get/<IMAGE_VFS_STORE>/<user_id>/<filename>`
|
||||
- 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 (`<IMAGE_VFS_URL>/api/vfs/get/<mount>/...`) 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 `<IMAGE_VFS_URL>/api/vfs/get/<IMAGE_VFS_STORE>/<uid>/<filename>`.
|
||||
- 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/<uid>/`.
|
||||
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.
|
||||
@ -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<string, string> = {};
|
||||
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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user