-- t-paliad-149 PR 2: per-user named card layouts. -- -- Design: docs/design-projects-page-2026-05-07.md §5b.3 (godel, -- m-locked 2026-05-07: full drag-rearrange + named layouts). -- -- Stores per-user named card-layout definitions for the /projects Cards -- view. A layout is a `(facts[], density, gridColumns, showAllLevels)` -- bundle plus a name and the is_default flag. -- -- The very first time a user opens Cards view, the application layer -- auto-seeds a "Standard" layout (the rich content set per design §5b.4) -- and flips its is_default=true. From there the user can rename, create -- new layouts, drag facts around, switch defaults, and delete (except -- the active default — UI gates). -- -- RLS scopes every operation to the calling user; layouts are personal -- working state (no firm-wide / cross-user visibility v1). Partial unique -- index keeps "at most one default per user" honest at the DB level even -- if the application layer's tx-flip-default ever races. -- ============================================================================ -- 1. paliad.user_card_layouts -- ============================================================================ CREATE TABLE paliad.user_card_layouts ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), user_id uuid NOT NULL REFERENCES paliad.users(id) ON DELETE CASCADE, -- Display name. Free-form; user picks the language they think in. -- Renders verbatim in the layout dropdown; no translation. name text NOT NULL, -- Exactly one default per user, enforced via partial unique index below. -- Application layer flips this in a transaction (clear old, set new). is_default boolean NOT NULL DEFAULT false, -- Layout JSON — see internal/services/layout_spec.go LayoutSpec. -- Validated on write; jsonb here for forward-compat without migrations -- as new fact keys land. layout_json jsonb NOT NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), -- Names are unique per user so the layout dropdown can use names as -- stable labels and the application layer can return ErrUserCardLayoutNameTaken. UNIQUE (user_id, name) ); -- ============================================================================ -- 2. Indexes -- ============================================================================ -- Hot path: list a user's layouts in name order. CREATE INDEX user_card_layouts_user_idx ON paliad.user_card_layouts (user_id, name); -- Partial unique index: at most one default layout per user. Keeps the -- invariant honest even if two concurrent PATCH .../set-default calls land -- (the second one's UPDATE will conflict, the application layer retries). CREATE UNIQUE INDEX user_card_layouts_default_idx ON paliad.user_card_layouts (user_id) WHERE is_default = true; -- ============================================================================ -- 3. RLS -- ============================================================================ ALTER TABLE paliad.user_card_layouts ENABLE ROW LEVEL SECURITY; -- Owner-only access. No global_admin override (mirrors paliad.user_views; -- card layouts are personal working state, not auditable infrastructure). CREATE POLICY user_card_layouts_owner_all ON paliad.user_card_layouts FOR ALL USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid());