30 KiB
Executable File
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
- Zero UX disruption — Tools work identically for everyone. Auth is invisible until the user wants it.
- Frontend stays king — All processing remains client-side. The backend is only for storage, auth, and proxy.
- Progressive enhancement — Each feature layer (auth → storage → proxy → billing) can ship independently.
- 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_URLtohttps://dewe.dev - Set
ADDITIONAL_REDIRECT_URLSfor localhost dev - Expose only the API gateway port (default 8000) and Studio port (default 3000)
1.2 Database Schema
-- 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)
-- 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:
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:
# 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:
- User works in editor as normal
- Clicks "Save" → if not logged in, AuthModal opens first
- Save dialog: enter file name (pre-filled with smart default)
- Saves to
user_filesvia Supabase SDK - Toast: "Saved to your account"
Load flow (new input tab):
- Tool input section gets a 5th tab:
[Create] [URL] [Paste] [Open] [My Files] - "My Files" tab shows a list of saved files for this tool type
- Each entry: name, date, size, [Load] [Delete] buttons
- 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
const {
files, // File[] for current tool_type
loading, // boolean
saving, // boolean
error, // string | null
saveFile, // (name, content, metadata?) => Promise<File>
updateFile, // (id, content, metadata?) => Promise<File>
deleteFile, // (id) => Promise<void>
loadFiles, // () => Promise<void> (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
// 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:
- Free users: Basic fetch (client-side, GET only, CORS-dependent) — current behavior
- Pro users: Toggle reveals advanced panel → requests go through Edge Function
- Update
features.jsto read tier fromuser.app_metadata.tierinstead of static value - Presets migration: move from localStorage to
user_fileswithtool_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:
- Navigates to the corresponding tool (e.g.,
/markdown-editor) - Passes file ID via query param:
/markdown-editor?file=uuid - Tool detects
?file=param → fetches fromuser_files→ loads into editor - Editor shows "Editing: {filename}" indicator
- 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
-
OAuth providers — GitHub OAuth is ideal for dev audience. Worth adding Google too for broader reach? Both are easy with Supabase Auth.
-
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_publicflag later. -
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).
-
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?
-
Delete account — GDPR requires account deletion. The
on delete cascadein 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