610dba78b7
Remove Cloudflare D1 entirely (4 API routes, lib/db/, Drizzle config/migrations, drizzle-orm/drizzle-kit deps, wrangler D1/R2/KV bindings) and replace with browser-local-first architecture: Open-source build (IndexedDB, no auth): - lib/persistence/ 5-file module: types, idb adapter (zero-dep, fault-tolerant, post-open invalidation retry), localStore (CRUD + sync-reserved metadata + slim/rebuild + retention-cap eviction with tombstone reap + sync-state protection + last-resort bounded fallback), sessionSlim (voice strip + styleRef absent-delete), cloudStore (Supabase skeleton, server-only) - Autosave: persistence fingerprint (history.length:lastBeatCount:playerName), serial saveChain, failure rollback retry, replaySourceRef guard (prevents replayed shared stories from clobbering user saves) - clientStoryPersistence.ts: thin facade (SaveResult discriminated union) - Stories page: /[locale]/stories with 3-language i18n (zh-CN/en/ja) - Homepage: book icon entry point in header Commercial build (Supabase, skeleton only): - Single table public.stories (JSONB + RLS 4 policies on auth.uid()=user_id) - supabase/migrations/ DDL (idempotent) - cloudStore.ts server-only repository, AUTH_ENABLED short-circuit - Not wired to client this phase Featured stories: pure fallback (buildFallbackCards + localizeCards), no D1 Includes fixes from 3 rounds of subagent code-review (tasks 16-30): - CR1: autosave restructure, coerceOrientation, D1 comment cleanup - CR2: fingerprint+serial+rollback+replay guard, idb post-open retry, enforceRetentionCap latent defense, sessionSlim absent invariant - CR3: single-scene share guard (replaySourceRef), insert-beat fingerprint (beats.length), pass3 overflow double-count fix, detach gate unification
55 lines
2.8 KiB
SQL
55 lines
2.8 KiB
SQL
-- Story persistence — Supabase single-table JSONB + RLS skeleton.
|
|
--
|
|
-- This migration lands the cloud schema for the COMMERCIAL build only. The
|
|
-- open-source build persists stories browser-locally (IndexedDB) and never
|
|
-- reaches this table. Cloud sync is NOT wired to any client this phase — the
|
|
-- table + RLS exist so next-phase local-first bidirectional sync can layer on
|
|
-- without a schema change.
|
|
--
|
|
-- One row mirrors the local StoryRecord's shared SlimStoryBlob payload
|
|
-- (lib/persistence/types.ts): list-view metadata is denormalized into columns
|
|
-- and the slim Session lives in session_jsonb. Per-user isolation is enforced
|
|
-- entirely by RLS (auth.uid() = user_id) against the SSR client's anon key +
|
|
-- user cookie — no service_role key is used.
|
|
--
|
|
-- Idempotent: safe to re-run. Tables/indexes use `if not exists`; policies are
|
|
-- dropped-then-created (Postgres has no `create policy if not exists`).
|
|
|
|
create table if not exists public.stories (
|
|
id text primary key, -- = Session.id ("s_xxx"), shared with the local record
|
|
user_id uuid not null references auth.users (id) on delete cascade,
|
|
world_setting text not null default '',
|
|
style_guide text not null default '',
|
|
orientation text not null default 'landscape', -- "portrait" | "landscape"
|
|
scene_count integer not null default 0,
|
|
rev integer not null default 1, -- revision; new = 1, +1 per save
|
|
created_at timestamptz not null default now(),
|
|
updated_at timestamptz not null default now(),
|
|
deleted_at timestamptz, -- soft-delete tombstone; null = live
|
|
session_jsonb jsonb not null -- slim Session blob (voice + styleReferenceImage stripped)
|
|
);
|
|
|
|
-- List query path: a user's stories newest-first.
|
|
create index if not exists stories_user_updated_idx
|
|
on public.stories (user_id, updated_at desc);
|
|
|
|
alter table public.stories enable row level security;
|
|
|
|
-- Authenticated users may read/write ONLY their own rows. Four policies, one
|
|
-- per command, all keyed on auth.uid() = user_id.
|
|
drop policy if exists "stories_select_own" on public.stories;
|
|
create policy "stories_select_own" on public.stories
|
|
for select using (auth.uid() = user_id);
|
|
|
|
drop policy if exists "stories_insert_own" on public.stories;
|
|
create policy "stories_insert_own" on public.stories
|
|
for insert with check (auth.uid() = user_id);
|
|
|
|
drop policy if exists "stories_update_own" on public.stories;
|
|
create policy "stories_update_own" on public.stories
|
|
for update using (auth.uid() = user_id) with check (auth.uid() = user_id);
|
|
|
|
drop policy if exists "stories_delete_own" on public.stories;
|
|
create policy "stories_delete_own" on public.stories
|
|
for delete using (auth.uid() = user_id);
|