Skip to content

Cloud Boundary

Cloud Boundary

CHANGE SUMMARY

  • Updated: Documented the Supabase Edge Function managed AI path plus the ai_usage_daily ledger (supabase/functions/anthropic-proxy/index.ts, supabase/migrations/0004_ai_usage_tracking.sql, 0005_upsert_ai_usage_fn.sql).
  • Updated: Differentiated BYOK vs. managed AI flows across the auth/security sections and captured the new client error mapping (src/api.js, src/supabase.js).
  • Updated (2026-04-26, SUR-261): Auth boundary now exposes signInWithGoogle / signInWithApple / requestEmailOtp / verifyEmailOtp / signOut / getSession (no signUp/signIn). Email-OTP is sign-in only via shouldCreateUser:false.

Skill in use: cloud-boundary — mapping Supabase schema, auth/storage boundaries, and risks.

1. Cloud services used

  • Supabase (Postgres + Auth + Storage): src/supabase.js instantiates a supabase-js client with import.meta.env.VITE_SUPABASE_URL + VITE_SUPABASE_ANON_KEY, so all persistence and sync calls run directly from the browser.
  • Supabase SQL migrations: Versioned files under supabase/migrations/*.sql define tables, policies, the note-images bucket, and now the AI usage ledger plus helper RPC (0004_ai_usage_tracking.sql, 0005_upsert_ai_usage_fn.sql). supabase/schema.sql explicitly says it is deprecated.
  • Schema contract tooling: scripts/schema-contract.js enumerates expected tables/policies while scripts/check-schema.js (invoked via npm run check:schema) verifies a live database via pg.
  • Supabase Edge Functions: supabase/functions/anthropic-proxy/index.ts is deployed to Supabase and proxies Anthropic for managed users; the client calls it via supabase.functions.invoke('anthropic-proxy', …) with the user’s JWT (src/supabase.js).
  • Supabase Storage: bucket note-images (created in 0001_initial_schema.sql) stores captured photos; uploads/downloads use supabase.storage.from('note-images') (src/supabase.js).
  • Anthropic: BYOK users still POST directly to Anthropic from the browser, while managed calls route browser -> Edge Function -> Anthropic (src/api.js).
  • Hosting: netlify.toml builds npm run build and serves /dist; there is no custom server tier besides Supabase.

2. Database schema

  • books / notes / custom_ideas: Mirror Dexie schemas with soft deletes, provenance columns, and owner-only RLS. Defined primarily in supabase/migrations/0001_initial_schema.sql, 0002_notes_add_source.sql, and 0003_notes_add_source_ingest.sql.
  • ai_usage_daily: Aggregate ledger storing per-user managed Anthropic usage (request_count, input_tokens, output_tokens) keyed by (user_id, action_type, window_day) plus RLS that only allows users to read their own rows (supabase/migrations/0004_ai_usage_tracking.sql).
  • upsert_ai_usage: SQL helper invoked via RPC to atomically insert-or-increment ai_usage_daily rows while running as the service role (supabase/migrations/0005_upsert_ai_usage_fn.sql).
  • Client code must continue to supply timestamps/flags; there are no triggers to coerce updated_at, so clock skew still affects conflict resolution.

3. Auth boundary

  • Supabase Auth remains the sole identity provider. Post-SUR-261, src/supabase.js exposes signInWithGoogle, signInWithApple, requestEmailOtp, verifyEmailOtp, signOut, and getSession wrappers — there is no signUp / signIn (password) wrapper.
  • Sign-up policy (in transition — SUR-358 Phase 1): every auth.users INSERT now triggers handle_new_auth_user (supabase/migrations/0020_generic_profile_creation_trigger.sql, SUR-362), which materialises a user_profiles row with default quota regardless of waitlist state — the bootstrap is decoupled from waitlist matching. The waitlist-EXISTS predicate that migration 0007 added to the RLS policies on books/notes/custom_ideas/storage.objects has been lifted by 0021_drop_waitlist_rls_gate.sql (SUR-363); ownership-only auth.uid() = user_id is now the sole access gate. Email-OTP currently pins shouldCreateUser:false (SUR-364 will flip this); the dashboard’s “Enable email signups” toggle stays disabled until the SUR-359 cutover.
  • useAuth subscribes to auth state, stores the active session, and injects session.user.id into every cloudWrite call before hitting Supabase tables (src/hooks/useAuth.js).
  • The managed Edge Function re-validates the caller’s JWT by creating an anon-key client and running supabase.auth.getUser(); failures return HTTP 401 before any Anthropic or usage call occurs (supabase/functions/anthropic-proxy/index.ts).
  • Because the anon key is bundled, RLS plus explicit user_id fields remain the primary safeguards against malicious clients.

4. Storage boundary

  • note-images stays private; paths are namespaced by auth.uid() via the “users manage own images” storage policy inside 0001_initial_schema.sql.
  • uploadImage writes {userId}/{noteId}.jpg with upsert: true and bubbles storage errors to the hook; download logic converts blobs back to base64 for Dexie hydration (src/supabase.js, src/hooks/useAuth.js).
  • The anthropic-proxy Edge Function only forwards JSON/text payloads and never persists media; managed AI usage touches Postgres only (no Storage writes).
  • There is still no automatic cleanup of storage objects when notes are deleted; retaining or purging blobs requires external tooling.

5. Security model

  • RLS: Policies defined in the migrations restrict books, notes, and custom_ideas CRUD to auth.uid() = user_id. scripts/schema-contract.js + scripts/check-schema.js act as the drift detector, and probeCloudSchema blocks sync inside useAuth.syncFromCloud if required columns disappear (src/hooks/useAuth.js).
  • Edge Function auth: Managed AI calls must include Authorization: Bearer <session.access_token>; the function verifies the JWT, sums usage with a service-role client, enforces the 30-call free-tier limit, and records successful calls through rpc('upsert_ai_usage', …). Any Supabase error throws and returns HTTP 500 so quota enforcement never fails open (supabase/functions/anthropic-proxy/index.ts).
  • Secrets: BYOK paths still expose Anthropic API keys from Dexie (src/db.js, src/api.js), but managed calls hide Anthropic credentials inside Supabase env vars (SUPABASE_SERVICE_ROLE_KEY, ANTHROPIC_API_KEY).
  • Outbox trust: cloudWrite/flushOutbox continue to send browser-crafted payloads via the anon key; there is no server-side validation beyond RLS (src/supabase.js).
  • Storage policy: The bucket policy limits reads/writes to the owner’s folder but does not limit file sizes or enforce lifecycle rules.

6. Gaps and risks

  • Manual migrations + functions: Deployers must apply supabase/migrations/*.sql and deploy the Edge Function separately; missing ai_usage_daily or the upsert RPC makes managed calls return HTTP 500 with no in-app remediation guidance (supabase/functions/anthropic-proxy/index.ts).
  • Schema probe cadence: useAuth only probes the schema once per load; transient failures still require a full reload to re-run the guard (src/hooks/useAuth.js).
  • Storage leaks: Deleting notes does not delete their images, so the private bucket grows unbounded (src/supabase.js).
  • BYOK exposure persists: Users supplying their own key still send prompts + keys directly to Anthropic with no proxy or quota, so compromised browsers remain a risk (src/api.js).
  • Anon key abuse: Anyone with the bundled anon key can script Supabase requests; RLS is the last line of defense.
  • No audit trail: Aside from ai_usage_daily, there is no logging of which device performed a write; investigations rely on Supabase logs outside this repo.