# Dewe.Dev — Accounts, File Storage & Pro Features Plan **Created:** February 18, 2026 --- ## Executive Summary Add user accounts and cloud file storage to dewe.dev without changing the existing UX. Visitors and logged-in users use the exact same tools. The only additions: a login/account page, save/load buttons per tool, and server-side URL fetching for Pro users. Backend powered by self-hosted Supabase on Coolify. --- ## Guiding Principles 1. **Zero UX disruption** — Tools work identically for everyone. Auth is invisible until the user wants it. 2. **Frontend stays king** — All processing remains client-side. The backend is only for storage, auth, and proxy. 3. **Progressive enhancement** — Each feature layer (auth → storage → proxy → billing) can ship independently. 4. **Privacy preserved** — File content is user-owned. RLS enforces isolation. No analytics on file content. --- ## Architecture Overview ``` ┌─────────────────────────────────────────────────────────┐ │ dewe.dev (React SPA) │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────────┐ │ │ │ MD │ │Invoice │ │ Table │ │ Object │ │ │ │ Editor │ │ Editor │ │ Editor │ │ Editor │ │ │ └────┬────┘ └────┬────┘ └────┬────┘ └─────┬─────┘ │ │ │ │ │ │ │ │ └────────────┴─────┬──────┴──────────────┘ │ │ │ │ │ ┌───────────┴───────────┐ │ │ │ useAuth() context │ │ │ │ useUserFiles() hook │ │ │ └───────────┬───────────┘ │ └──────────────────────────┼──────────────────────────────┘ │ HTTPS (Supabase JS SDK) │ ┌──────────────────────────┼──────────────────────────────┐ │ Self-hosted Supabase (Coolify) │ │ │ │ │ ┌───────────┐ ┌────────┴────────┐ ┌───────────────┐ │ │ │ GoTrue │ │ PostgREST │ │ Edge Functions│ │ │ │ (Auth) │ │ (REST API) │ │ (CORS Proxy) │ │ │ └───────────┘ └────────┬────────┘ └───────────────┘ │ │ │ │ │ ┌────────┴────────┐ │ │ │ PostgreSQL │ │ │ │ │ │ │ │ • auth.users │ │ │ │ • profiles │ │ │ │ • user_files │ │ │ │ • proxy_logs │ │ │ └─────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` --- ## Phase 1: Supabase Setup on Coolify **Goal:** Get the backend running and verified before writing any frontend code. ### 1.1 Deploy Supabase on Coolify Coolify supports Supabase as a one-click service (or via Docker Compose). **Resource Requirements:** | Resource | Minimum | Recommended | |----------|---------|-------------| | CPU | 2 cores | 4 cores | | RAM | 4 GB | 8 GB | | Disk | 20 GB SSD | 50 GB SSD | **Services that spin up:** - PostgreSQL (database) - GoTrue (auth) - PostgREST (auto-generated REST API) - Realtime (websockets — can disable if unused) - Storage API (file storage — optional for this use case) - Edge Runtime (Deno functions) - Studio (admin dashboard) - Kong/Envoy (API gateway) **Critical Coolify configuration:** - Map persistent volumes for PostgreSQL data directory — data survives container restarts - Set up SSL via Coolify's built-in Let's Encrypt - Configure `SITE_URL` to `https://dewe.dev` - Set `ADDITIONAL_REDIRECT_URLS` for localhost dev - Expose only the API gateway port (default 8000) and Studio port (default 3000) ### 1.2 Database Schema ```sql -- Profiles table (extends auth.users) create table profiles ( id uuid primary key references auth.users on delete cascade, display_name text, tier text not null default 'free' check (tier in ('free', 'pro')), tier_expires_at timestamptz, storage_used_bytes bigint default 0, created_at timestamptz default now(), updated_at timestamptz default now() ); -- Auto-create profile on signup create or replace function handle_new_user() returns trigger as $$ begin insert into profiles (id, display_name) values (new.id, coalesce(new.raw_user_meta_data->>'display_name', split_part(new.email, '@', 1))); return new; end; $$ language plpgsql security definer; create trigger on_auth_user_created after insert on auth.users for each row execute function handle_new_user(); -- Sync tier to JWT custom claims (app_metadata) create or replace function sync_tier_to_claims() returns trigger as $$ begin update auth.users set raw_app_meta_data = raw_app_meta_data || jsonb_build_object('tier', new.tier) where id = new.id; return new; end; $$ language plpgsql security definer; create trigger on_tier_change after update of tier on profiles for each row execute function sync_tier_to_claims(); -- User files table create table user_files ( id uuid primary key default gen_random_uuid(), user_id uuid references auth.users on delete cascade not null, tool_type text not null, -- 'markdown', 'invoice', 'object', 'table', 'beautifier' name text not null, content jsonb not null, -- The actual file data metadata jsonb default '{}', -- Optional: last export format, preview text, etc. size_bytes integer generated always as (octet_length(content::text)) stored, created_at timestamptz default now(), updated_at timestamptz default now() ); -- Indexes create index idx_user_files_user_tool on user_files(user_id, tool_type); create index idx_user_files_updated on user_files(user_id, updated_at desc); -- Proxy usage logging (for rate limiting & analytics) create table proxy_logs ( id bigserial primary key, user_id uuid references auth.users not null, target_url text not null, method text not null, status_code integer, response_size integer, duration_ms integer, created_at timestamptz default now() ); create index idx_proxy_logs_user_time on proxy_logs(user_id, created_at desc); ``` ### 1.3 Row Level Security (RLS) ```sql -- Profiles: users can read/update only their own alter table profiles enable row level security; create policy "Users can view own profile" on profiles for select using (auth.uid() = id); create policy "Users can update own profile" on profiles for update using (auth.uid() = id); -- User files: full CRUD, own files only alter table user_files enable row level security; create policy "Users can view own files" on user_files for select using (auth.uid() = user_id); create policy "Users can insert own files" on user_files for insert with check (auth.uid() = user_id); create policy "Users can update own files" on user_files for update using (auth.uid() = user_id); create policy "Users can delete own files" on user_files for delete using (auth.uid() = user_id); -- Proxy logs: users can view own logs alter table proxy_logs enable row level security; create policy "Users can view own proxy logs" on proxy_logs for select using (auth.uid() = user_id); ``` ### 1.4 Storage Limits | Tier | Max files | Max file size | Total storage | |------|-----------|---------------|---------------| | Free | 20 | 256 KB | 5 MB | | Pro | Unlimited | 2 MB | 100 MB | Enforced via database function: ```sql create or replace function check_storage_limit() returns trigger as $$ declare current_count integer; current_size bigint; user_tier text; max_count integer; max_total bigint; begin select tier into user_tier from profiles where id = new.user_id; if user_tier = 'free' then max_count := 20; max_total := 5 * 1024 * 1024; -- 5 MB else max_count := 10000; max_total := 100 * 1024 * 1024; -- 100 MB end if; select count(*), coalesce(sum(size_bytes), 0) into current_count, current_size from user_files where user_id = new.user_id; if current_count >= max_count then raise exception 'File limit reached (% files for % tier)', max_count, user_tier; end if; if current_size + octet_length(new.content::text) > max_total then raise exception 'Storage limit reached (% bytes for % tier)', max_total, user_tier; end if; return new; end; $$ language plpgsql; create trigger check_storage_before_insert before insert on user_files for each row execute function check_storage_limit(); ``` ### 1.5 Backups Self-hosted = you own backups. Set up immediately: ```bash # Cron job on Coolify host (daily at 3 AM) 0 3 * * * pg_dump -h localhost -U postgres -d postgres | gzip > /backups/dewe_$(date +\%Y\%m\%d).sql.gz # Retention: keep 30 days find /backups -name "dewe_*.sql.gz" -mtime +30 -delete ``` Optional: pipe to S3-compatible storage (MinIO on Coolify, or external). --- ## Phase 2: Auth Integration (Frontend) **Goal:** Add authentication without changing any existing tool UX. ### 2.1 Auth Provider Setup ``` src/ ├── contexts/ │ └── AuthContext.js # Supabase auth state ├── hooks/ │ ├── useAuth.js # Auth convenience hook │ └── useUserFiles.js # File CRUD hook ├── components/ │ ├── AuthModal.js # Login/signup modal (not a page redirect) │ ├── UserMenu.js # Header avatar/dropdown │ └── SaveLoadBar.js # Save/Load UI for tools ├── pages/ │ └── AccountPage.js # /account - profile, files, settings └── lib/ └── supabase.js # Supabase client init ``` ### 2.2 "Ghost Auth" Pattern The core UX principle: **tools never change behavior based on auth state.** ``` ┌──────────────────────────────────────────────────┐ │ Tool Editor │ │ │ │ Visitor sees: │ Logged-in user sees: │ │ [Copy] [Download] │ [Copy] [Download] [Save] │ │ │ ↑ │ │ │ only addition │ └──────────────────────────────────────────────────┘ ``` **Rules:** - No redirects to login page from tools. Ever. - Clicking "Save" when not logged in → opens AuthModal (login/signup) as overlay - Current editor state is preserved in React state during login - After successful auth, the save action proceeds automatically - "Load" button appears in the tool's input section (alongside Create/URL/Paste/Open tabs) ### 2.3 Auth Methods | Method | Priority | Notes | |--------|----------|-------| | Email + Password | P0 | Core auth, must have | | GitHub OAuth | P1 | Target audience is developers | | Google OAuth | P2 | Broad reach, nice to have | | Magic Link (Email) | P2 | Passwordless option | ### 2.4 Header Changes ``` Before (visitor): ┌─────────────────────────────────────────────────┐ │ [Logo] [Tools ▾] [🌙] [☰] │ └─────────────────────────────────────────────────┘ After (visitor): ┌─────────────────────────────────────────────────┐ │ [Logo] [Tools ▾] [🌙] [Sign In] [☰]│ └─────────────────────────────────────────────────┘ After (logged in): ┌─────────────────────────────────────────────────┐ │ [Logo] [Tools ▾] [🌙] [Avatar ▾] [☰]│ └─────────────────────────────────────────────────┘ Avatar dropdown: ├── Account & Files ├── ───────────── └── Sign Out ``` Minimal. No dashboard. The "Account & Files" link goes to `/account`. --- ## Phase 3: File Save/Load per Tool **Goal:** Each editor tool gets save/load capability for logged-in users. ### 3.1 Which Tools Support Save/Load | Tool | Save Content | Content Format | Priority | |------|-------------|----------------|----------| | Markdown Editor | MD source text | `{ source: string }` | P0 | | Invoice Editor | Invoice data | `{ invoice: {...}, template: string }` | P0 | | Object Editor | JSON/PHP data | `{ input: string, format: string }` | P1 | | Table Editor | Table data | `{ headers: [...], rows: [...] }` | P1 | | Beautifier | Code input | `{ code: string, language: string }` | P2 | | Diff Tool | Both texts | `{ left: string, right: string }` | P2 | URL Encoder, Base64, and Text Length are **stateless tools** — no save/load needed. ### 3.2 Save/Load UX per Tool **Save flow:** 1. User works in editor as normal 2. Clicks "Save" → if not logged in, AuthModal opens first 3. Save dialog: enter file name (pre-filled with smart default) 4. Saves to `user_files` via Supabase SDK 5. Toast: "Saved to your account" **Load flow (new input tab):** 1. Tool input section gets a 5th tab: `[Create] [URL] [Paste] [Open] [My Files]` 2. "My Files" tab shows a list of saved files for this tool type 3. Each entry: name, date, size, [Load] [Delete] buttons 4. Loading replaces current editor content (with unsaved changes warning) **Smart defaults for file names:** - Markdown: first heading or "Untitled Document" - Invoice: "Invoice #{number} - {client_name}" - Table: "Table - {row_count} rows" - Object: "Object - {key_count} keys" ### 3.3 `useUserFiles` Hook API ```javascript const { files, // File[] for current tool_type loading, // boolean saving, // boolean error, // string | null saveFile, // (name, content, metadata?) => Promise updateFile, // (id, content, metadata?) => Promise deleteFile, // (id) => Promise loadFiles, // () => Promise (refresh) storageUsed, // { count, bytes, limit } } = useUserFiles('markdown'); ``` --- ## Phase 4: Pro Proxy (Server-Side URL Fetch) **Goal:** Pro users bypass CORS with a server-side proxy via Supabase Edge Function. ### 4.1 How It Works ``` Free user: Pro user: Browser → Target API Browser → Edge Function → Target API ↓ ↓ CORS blocked ❌ No CORS ✅ ``` ### 4.2 Edge Function: `proxy-fetch` ```typescript // supabase/functions/proxy-fetch/index.ts import { serve } from "https://deno.land/std/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js"; const BLOCKED_HOSTS = [ /^localhost$/i, /^127\./, /^10\./, /^172\.(1[6-9]|2\d|3[01])\./, /^192\.168\./, /^0\./, /^169\.254\./, // link-local /^::1$/, /^fc00:/i, /^fe80:/i, // IPv6 private ]; serve(async (req) => { // 1. Verify JWT and extract tier const authHeader = req.headers.get("Authorization"); const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { global: { headers: { Authorization: authHeader } } }); const { data: { user }, error } = await supabase.auth.getUser(); if (!user || user.app_metadata?.tier !== "pro") { return new Response(JSON.stringify({ error: "Pro subscription required" }), { status: 403 }); } // 2. Parse request const { url, method, headers, body } = await req.json(); // 3. SSRF protection const targetUrl = new URL(url); if (BLOCKED_HOSTS.some(pattern => pattern.test(targetUrl.hostname))) { return new Response(JSON.stringify({ error: "Blocked host" }), { status: 400 }); } // 4. Rate limiting check (50 req/min for Pro) const { count } = await supabase .from("proxy_logs") .select("*", { count: "exact", head: true }) .eq("user_id", user.id) .gte("created_at", new Date(Date.now() - 60000).toISOString()); if (count >= 50) { return new Response(JSON.stringify({ error: "Rate limit exceeded (50/min)" }), { status: 429 }); } // 5. Forward request const start = Date.now(); const response = await fetch(url, { method: method || "GET", headers: headers || {}, body: body ? JSON.stringify(body) : undefined, }); const responseBody = await response.text(); // 6. Log usage await supabase.from("proxy_logs").insert({ user_id: user.id, target_url: url, method: method || "GET", status_code: response.status, response_size: responseBody.length, duration_ms: Date.now() - start, }); // 7. Return response return new Response(responseBody, { status: response.status, headers: { "Content-Type": response.headers.get("Content-Type") || "application/json", "Access-Control-Allow-Origin": "https://dewe.dev", }, }); }); ``` ### 4.3 Frontend Integration The existing `AdvancedURLFetch.js` component already has the full UI (method, headers, auth, body, presets). Currently hidden behind `{false && ...}`. Changes needed: 1. **Free users:** Basic fetch (client-side, GET only, CORS-dependent) — current behavior 2. **Pro users:** Toggle reveals advanced panel → requests go through Edge Function 3. Update `features.js` to read tier from `user.app_metadata.tier` instead of static value 4. Presets migration: move from localStorage to `user_files` with `tool_type: 'fetch_preset'` ### 4.4 Security Measures | Threat | Mitigation | |--------|-----------| | SSRF (hitting internal services) | Block private IP ranges, resolve DNS before fetch | | Abuse (crypto mining, spam) | Rate limiting (50 req/min), response size cap (5 MB) | | Data exfiltration | Log all proxy requests, alert on anomalies | | JWT forgery | Verify JWT server-side on every request | | Cost attack (huge responses) | Stream with size limit, abort after 5 MB | --- ## Phase 5: Account Page **Goal:** Single page at `/account` for profile, files, and settings. Minimal. ### 5.1 Page Layout ``` /account ┌─────────────────────────────────────────────────┐ │ Profile │ │ ┌─────────────────────────────────────────────┐ │ │ │ [Avatar] display_name tier_badge │ │ │ │ email │ │ │ │ Member since: date │ │ │ │ [Edit Profile] │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ Storage: ████████░░░░ 2.3 MB / 5 MB (Free) │ │ [Upgrade to Pro] │ │ │ │ My Files [Filter ▾] │ │ ┌─────────────────────────────────────────────┐ │ │ │ 📝 Project README.md 12 KB 2h ago │ │ │ │ 📄 Invoice #042 - Acme 3 KB 1d ago │ │ │ │ 📊 Sales Data Q4 8 KB 3d ago │ │ │ │ { } API Response Sample 1 KB 1w ago │ │ │ └─────────────────────────────────────────────┘ │ │ │ │ Each file row: [Open in Editor] [Download] [🗑] │ │ │ │ ────────────────────────────────────────────── │ │ [Sign Out] [Delete Account] │ └─────────────────────────────────────────────────┘ ``` ### 5.2 "Open in Editor" Flow Clicking "Open in Editor" on a saved file: 1. Navigates to the corresponding tool (e.g., `/markdown-editor`) 2. Passes file ID via query param: `/markdown-editor?file=uuid` 3. Tool detects `?file=` param → fetches from `user_files` → loads into editor 4. Editor shows "Editing: {filename}" indicator 5. Subsequent saves update the same file (not create new) --- ## Phase 6: localStorage Migration **Goal:** Seamlessly migrate existing localStorage data to cloud on first login. ### 6.1 Migration Flow ``` User logs in for the first time │ ▼ Check localStorage for known keys: • urlFetchPresets • (any other persisted tool data) │ ▼ Found data? ──No──→ Done │ Yes │ ▼ Show toast: "We found saved data on this device. Would you like to sync it to your account?" │ ┌────┴────┐ Yes No │ │ ▼ ▼ Upload Dismiss to DB (ask again + clear next login) localStorage ``` ### 6.2 Known localStorage Keys to Migrate | Key | Tool | Migration Target | |-----|------|-----------------| | `urlFetchPresets` | AdvancedURLFetch | `user_files` with `tool_type: 'fetch_preset'` | Currently this is the only localStorage key with user data. As tools add auto-save to localStorage (pre-auth), add those keys here. --- ## Phase 7: Pro Tier & Billing (Future) **Goal:** Monetize Pro features. Not in initial scope but schema is ready. ### 7.1 Pro Features Summary | Feature | Free | Pro | |---------|------|-----| | All tools | Yes | Yes | | Copy & Download | Yes | Yes | | Save files | 20 files, 5 MB | Unlimited, 100 MB | | URL Fetch | Client-side (CORS-dependent) | Server-side proxy (any API) | | Fetch presets | localStorage only | Cloud-synced | | Advanced Fetch UI | Hidden | Full (methods, headers, auth, body) | | Ads | Yes | No | ### 7.2 Pricing (Recommendation) **Subscription model** (recurring revenue > one-time): | Plan | Price | Billing | |------|-------|---------| | Monthly | $4.99/mo | Monthly | | Annual | $39.99/yr (~$3.33/mo) | Annual | **Why subscription over one-time:** - Server costs are ongoing (Coolify hosting, proxy bandwidth) - Aligns cost with value delivered - Predictable revenue **Payment provider:** Stripe (or Lemon Squeezy for simpler tax handling). ### 7.3 Tier Enforcement Architecture ``` Client-side (fast): user.app_metadata.tier → show/hide UI elements Server-side (secure): JWT claims → Edge Function checks tier before processing DB triggers → sync tier changes to JWT claims Storage limits → DB trigger on insert ``` The `sync_tier_to_claims` trigger (Phase 1 schema) ensures that when you update `profiles.tier`, the JWT custom claim is updated. The user's next token refresh picks up the new tier automatically. --- ## Implementation Order ``` Phase 1: Supabase on Coolify ░░░░░░░░ ~1 day └─ Deploy, configure, run schema └─ Verify connection from localhost Phase 2: Auth Integration ░░░░░░░░░░░░ ~2 days └─ supabase.js client setup └─ AuthContext + useAuth hook └─ AuthModal (login/signup overlay) └─ Header UserMenu └─ /account page (basic) Phase 3: File Save/Load ░░░░░░░░░░░░░░░░ ~3 days └─ useUserFiles hook └─ SaveLoadBar component └─ "My Files" tab in each tool └─ Account page file manager └─ Markdown Editor integration (first) └─ Invoice Editor integration └─ Remaining tools Phase 4: Pro Proxy ░░░░░░░░░░░░ ~2 days └─ Edge Function deployment └─ SSRF protection └─ Rate limiting └─ AdvancedURLFetch rewire └─ Feature flags from JWT Phase 5: localStorage Migration ░░░░ ~0.5 day └─ Detection + migration prompt Phase 6: Polish & Testing ░░░░░░░░ ~1.5 days └─ Error states, loading states └─ Mobile responsive check └─ Auth edge cases (expired session, etc.) Total estimate: ~10 days ``` --- ## Technical Decisions & Rationale ### Why JSONB in Postgres (not Supabase Storage)? All saved content is text-based and < 2 MB. JSONB gives us: - Queryable content (search inside saved files later) - Indexable fields (filter by tool_type, sort by date) - Single table, single query — no separate file download step - Atomic operations (save file + update metadata in one transaction) - Simpler backup (one pg_dump gets everything) Supabase Storage (S3-compatible) would be better for binary files (images, PDFs > 10 MB). We don't have that use case. ### Why Edge Functions (not a separate Node.js proxy)? - Already included in self-hosted Supabase — no extra deployment - Direct access to Supabase Auth (JWT verification built-in) - Deno runtime is secure by default (explicit permissions) - Scales with the Supabase instance - One less service to maintain on Coolify ### Why Modal Login (not a /login page)? - User is mid-work in an editor when they hit "Save" - Redirecting to /login would lose their unsaved editor state - Modal keeps React state intact — after login, save proceeds immediately - Better conversion: lower friction = more signups ### Why Custom Claims for Tier (not DB lookup per request)? - JWT claims are verified locally — zero network latency - Edge Function doesn't need a DB query to check tier - DB trigger keeps claims in sync automatically - Standard Supabase pattern, well-documented --- ## Risks & Mitigations | Risk | Impact | Mitigation | |------|--------|-----------| | Supabase self-hosted instability | High | Monitor with Coolify health checks, set up alerts, daily backups | | Proxy abuse (SSRF, scraping) | High | IP blocklist, rate limiting, URL validation, response size cap | | Storage costs grow | Medium | Enforce limits per tier, compress JSONB, monitor `storage_used_bytes` | | Auth session expired mid-edit | Medium | Silent token refresh, save to localStorage as fallback, retry on 401 | | Migration from localStorage fails | Low | Non-destructive: keep localStorage as fallback, retry option | | Coolify resource limits | Medium | Start with 4 GB RAM, monitor, scale vertically as needed | --- ## Open Questions 1. **OAuth providers** — GitHub OAuth is ideal for dev audience. Worth adding Google too for broader reach? Both are easy with Supabase Auth. 2. **File sharing** — Should saved files be shareable via public link? (e.g., share a markdown preview). Not in initial scope, but schema supports adding a `is_public` flag later. 3. **Auto-save** — Should tools auto-save to cloud every N seconds for logged-in users? Or only manual save? Recommendation: manual save only (simpler, less backend load, user controls what gets saved). 4. **Offline support** — If Supabase is down, tools still work (client-side). But save/load fails. Should we queue saves in localStorage and sync when back online? 5. **Delete account** — GDPR requires account deletion. The `on delete cascade` in the schema handles this. Need a confirmation flow in the UI. --- ## File Structure Changes ``` src/ ├── lib/ │ └── supabase.js # NEW: Supabase client init ├── contexts/ │ └── AuthContext.js # NEW: Auth state provider ├── hooks/ │ ├── useAuth.js # NEW: Auth convenience hook │ ├── useUserFiles.js # NEW: File CRUD operations │ ├── useAnalytics.js │ └── useNavigationGuard.js ├── components/ │ ├── AuthModal.js # NEW: Login/signup modal overlay │ ├── UserMenu.js # NEW: Avatar dropdown in header │ ├── SaveLoadBar.js # NEW: Save/Load buttons for tools │ ├── MyFilesTab.js # NEW: "My Files" input tab │ ├── StorageMeter.js # NEW: Storage usage indicator │ ├── AdvancedURLFetch.js # MODIFY: use proxy for Pro │ ├── ProBadge.js # KEEP: already exists │ └── Layout.js # MODIFY: add UserMenu to header ├── pages/ │ ├── AccountPage.js # NEW: /account │ └── ...existing tools... # MODIFY: add save/load integration ├── config/ │ ├── features.js # MODIFY: read tier from JWT claims │ └── tools.js # KEEP └── App.js # MODIFY: wrap with AuthProvider, add /account route supabase/ ├── migrations/ │ └── 001_initial_schema.sql # NEW: all tables, RLS, triggers └── functions/ └── proxy-fetch/ └── index.ts # NEW: CORS proxy edge function ```