Files
dewedev/PLAN_ACCOUNTS_AND_STORAGE.md

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

  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

-- 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:

  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

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:

  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