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(nosignUp/signIn). Email-OTP is sign-in only viashouldCreateUser:false.
Skill in use: cloud-boundary — mapping Supabase schema, auth/storage boundaries, and risks.
1. Cloud services used
- Supabase (Postgres + Auth + Storage):
src/supabase.jsinstantiates a supabase-js client withimport.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/*.sqldefine tables, policies, thenote-imagesbucket, and now the AI usage ledger plus helper RPC (0004_ai_usage_tracking.sql,0005_upsert_ai_usage_fn.sql).supabase/schema.sqlexplicitly says it is deprecated. - Schema contract tooling:
scripts/schema-contract.jsenumerates expected tables/policies whilescripts/check-schema.js(invoked vianpm run check:schema) verifies a live database viapg. - Supabase Edge Functions:
supabase/functions/anthropic-proxy/index.tsis deployed to Supabase and proxies Anthropic for managed users; the client calls it viasupabase.functions.invoke('anthropic-proxy', …)with the user’s JWT (src/supabase.js). - Supabase Storage: bucket
note-images(created in0001_initial_schema.sql) stores captured photos; uploads/downloads usesupabase.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.tomlbuildsnpm run buildand 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, and0003_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.jsexposessignInWithGoogle,signInWithApple,requestEmailOtp,verifyEmailOtp,signOut, andgetSessionwrappers — there is nosignUp/signIn(password) wrapper. - Sign-up policy (in transition — SUR-358 Phase 1): every
auth.usersINSERT now triggershandle_new_auth_user(supabase/migrations/0020_generic_profile_creation_trigger.sql, SUR-362), which materialises auser_profilesrow 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 onbooks/notes/custom_ideas/storage.objectshas been lifted by0021_drop_waitlist_rls_gate.sql(SUR-363); ownership-onlyauth.uid() = user_idis now the sole access gate. Email-OTP currently pinsshouldCreateUser:false(SUR-364 will flip this); the dashboard’s “Enable email signups” toggle stays disabled until the SUR-359 cutover. useAuthsubscribes to auth state, stores the active session, and injectssession.user.idinto everycloudWritecall 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-imagesstays private; paths are namespaced byauth.uid()via the “users manage own images” storage policy inside0001_initial_schema.sql.uploadImagewrites{userId}/{noteId}.jpgwithupsert: trueand 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, andcustom_ideasCRUD toauth.uid() = user_id.scripts/schema-contract.js+scripts/check-schema.jsact as the drift detector, andprobeCloudSchemablocks sync insideuseAuth.syncFromCloudif 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 throughrpc('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/flushOutboxcontinue 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/*.sqland deploy the Edge Function separately; missingai_usage_dailyor theupsertRPC makes managed calls return HTTP 500 with no in-app remediation guidance (supabase/functions/anthropic-proxy/index.ts). - Schema probe cadence:
useAuthonly 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.