feat(marquee): Phase 3 auth + RBAC + own-only security (both backends) - #53
Merged
Merged
Conversation
The first "SDK gap" phase. Finding: joins and aggregation do NOT need a core
change. They ride the existing named-operation port (`store.run`), whose calling
convention is portable but whose implementation is per-backend, which is exactly
the seam the core reserves for where backends genuinely diverge. The capability
flags (serverSideJoins / aggregations) declare the divergence; the UI surfaces it.
Schema (both backends): people + credits make cast/director a real many-to-many
relation; Supabase adds FK constraints (for PostgREST embeds) and a genre_counts
SQL view.
Named queries, same shapes, divergent implementations:
- movieCredits (the JOIN): Supabase does a server-side PostgREST embed over the
FKs; Convex (convex/marquee.ts) and memory follow refs by hand.
- genreCounts (the AGGREGATION): Supabase reads a GROUP BY view; Convex and memory
scan and tally (the cost the `aggregations: false` flag warns about, and which
Phase 5 scale will exercise).
makeBackend wires the typed MarqueeSchema with per-backend query impls (src/lib/
enrich.ts, kept env-free so tests build typed backends directly). The catalog
functions are generic over the schema so the bare test backends still fit.
UI: MovieDetail shows the real joined cast (director + billed actors with
characters); the catalog shows per-genre counts. Both render an understated
caption declaring HOW the data loaded on the active backend ("Cast joined
server-side" vs "assembled by following references"; "Counted with a SQL view"
vs "by scanning") -- the thin-honest-waist thesis made visible.
Verification: enrich.live.test.ts (12 tests: join order + name resolution, empty
cast, aggregation, capability declaration) green on memory + Supabase + Convex;
catalog gate still 20/20. A Playwright UI smoke confirmed the cast join and the
genre-stats strip with the correct divergence captions on both live backends.
avg-rating aggregation defers to Phase 3 (reviews need auth); Phase 2's
aggregation is genre counts over existing data.
The decision-complete Phase 3 plan (auth + RBAC + own-only security) and the Convex auth.config.ts that verifies the shared OIDC issuer's JWT. Topology (decided): a pluggable IdentityProvider behind a shared OIDC issuer (Supabase default), so identity is consistent across backends (Phase 5 migration stays coherent) and Convex is not Supabase-locked (swap the issuer, not Supabase by law). Independent Convex credential auth (@convex-dev/auth) logged as a future SDK capability, not built now. Verified this session: the handshake infrastructure lines up (Supabase OIDC discovery + ES256 JWKS; iss/aud/sub claims; Convex accepted auth.config). The runtime verify is exercised by the Phase 3 security tests (next sub-steps).
The user-owned tables for auth/RBAC, on both backends: - Supabase migration 0003: profiles (userId, role, displayName) + reviews (movieId, userId, rating 1-5, body), with Row Level Security enforcing own-only writes at the DATABASE (reviews_insert/update/delete_own use auth.uid()), plus an auth_role() SECURITY DEFINER helper for later catalog gating. SELECT is open (guests browse, reviewer names show). A unique (userId, movieId) index makes a review one-per-user-per-movie. - Convex schema: profiles + reviews tables with by_userId / by_movieId indexes. userId is the shared-issuer subject (Supabase auth uid), so identity matches across backends and survives the Phase 5 migration. Catalog tables are untouched (their writes get role-gated via named mutations in sub-step 3), so the Phase 1/2 portable-CRUD tests and the seed keep working. Verified: both tables resolve on Supabase (anon SELECT under RLS) and Convex (indexes deployed).
The acceptance bar, green on Supabase AND Convex: a member cannot edit or delete
another member's review, rejected by the BACKEND, with the review left unchanged.
Proven red-first (temporarily removing the Convex ownership check fails the test;
restored).
- Review writes are named MUTATIONS (addReview/editReview/deleteReview), off the
generic portable CRUD, with identity taken from the verified session, never args:
- Supabase: thin PostgREST calls on the user's authed client; RLS is the arbiter
(a cross-user edit matches 0 rows -> explicit rejection).
- Convex: deployed mutations (convex/reviews.ts) checking ctx.auth.getUserIdentity().
- Memory: open dev sandbox (no auth), a single synthetic user.
- movieRating(movieId) -> {avg,count}, the aggregation deferred from Phase 2.
- AUTH HANDSHAKE PROVEN on the local stack: Convex verifies the Supabase ES256 JWT.
Finding: the OIDC-discovery provider expects an HTTPS issuer, so the local HTTP
Supabase fails discovery; the `customJwt` provider (explicit JWKS URL + ES256)
works with the local issuer. auth.config.ts switched to customJwt.
- Tests run serially (vitest.config.ts fileParallelism:false): the live suites share
one DB and each resets+owns it, so parallel files corrupt each other's counts.
Full suite 40/40 (catalog 20, enrich 12, security 8).
Schema/types: MarqueeSchema gains the review mutations + movieRating; the Phase 2
enrich test now builds complete typed backends.
The Marquee dogfood (Phase 3 detail page) found that `store.get()` on Supabase
returned the raw PostgREST row keyed only by the primary-key column, so a fetched
document had no `_id`, an inconsistency with `store.list()` (which maps it) and
with the Convex adapter (whose docs carry `_id` natively). A doc fetched via
`get()` therefore could not be passed straight to `patch()`/`remove()`.
get() now maps `{ _id: <pk>, ...row }` exactly as list() does. The conformance
round-trip test now asserts the fetched doc carries `_id` (passes on memory; the
supabase/convex live conformance jobs cover the other two). Patch via changeset.
…th backends) The user-facing half of Phase 3, verified in the browser on both live backends. - lib/identity.ts: the pluggable IdentityProvider seam (Supabase default issuer), so the issuer is swappable and Convex is not Supabase-locked. - lib/auth.tsx: AuthProvider + useAuth. One issuer signs in; the ACTIVE backend is wired per capability (Supabase seats a session for RLS; Convex setToken-verifies; memory is open). Switching backends re-wires from the same session, so it is ONE login across both. Role comes from a profiles row (default member). - AuthBar: header sign-in/up/out + email + role badge. - ReviewSection: avg rating + count, the signed-in user's own add/edit/delete form (addReview upserts), other users' reviews read-only. All enforcement is backend. - Gating (convenience): "+ Add movie" and detail "Edit" only for editor/admin. Browser smoke (LAN IP, both backends, 0 console errors): sign in as a member -> post a review -> rating aggregates -> own-review controls appear -> "+ Add movie" hidden. Switch Supabase<->Convex stays signed in (Convex verifies the Supabase JWT live) and the review flow works on each. Two robustness fixes the smoke surfaced: MovieDetail used movie._id (now the canonical movieId prop) and assumed movie.genres exists (Convex, being schemaless, omits a never-written field that Supabase fills from a column default; now guarded).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #52. Phase 3 delivers real, backend-enforced auth and own-only review security, plus the avg-rating aggregation deferred from Phase 2. Verified green on Supabase AND Convex (data-layer tests + a live browser smoke), and it surfaced one genuine SDK bug (fixed here).
Auth model (decided with the maintainer)
A pluggable
IdentityProviderbehind a shared OIDC issuer (Supabase default). One sign-in; the active data backend is wired per capability: Supabase seats a session (RLS seesauth.uid()), ConvexsetToken-verifies the same JWT (managesCredentials:false). Switching backends re-wires from the same session, so it is one login across both. Convex is not Supabase-locked: swap the issuer and Convex verifies that instead. Roles live in aprofilesrow.Security (the acceptance bar, green on both)
Review writes are named mutations (not the generic CRUD), identity taken from the verified session never args:
reviews_*_ownpolicies; a cross-user edit hits 0 rows -> explicit rejection).ctx.auth.getUserIdentity().test/security.live.test.ts(8/8 on both): a member cannot edit/delete another member's review (backend rejects, row unchanged), own-review CRUD works, signed-out writes rejected. Proven red-first (removing the Convex check fails the test). Full suite 40/40 (serialized; the live suites share one DB).Auth handshake finding
Convex's OIDC-discovery provider expects an HTTPS issuer, so the local HTTP Supabase fails discovery (
getUserIdentity()is null). ThecustomJwtprovider (explicit JWKS URL +ES256) verifies the local issuer.auth.config.tsuses it.SDK bug found + fixed (the dogfood earning its keep)
@baas/adapter-supabasestore.get()returned the raw row without the portable_id(an inconsistency withlist()and with Convex). Fixed to map_idlikelist(); conformance round-trip now asserts it. Patch changeset included.Browser smoke (both backends, 0 console errors)
Sign in as a member -> post a review -> rating aggregates -> own-review controls appear -> "+ Add movie"/Edit hidden (member). Switch Supabase<->Convex: stays signed in (Convex verifies the Supabase JWT live), review flow works on each. Two robustness fixes the smoke surfaced (a
movie._idvs canonical-prop slip, andmovie.genresundefined on schemaless Convex) are included.Remaining (not blocking the phase): server-side catalog write-gating is currently a UI convenience (reviews are the DB-enforced headline); reviewer display names are not shown (reviews carry only the opaque subject). Both noted as follow-ups.