Parse → Supabase DB foundation: read-scope schema, sample importer, local dev#9
Parse → Supabase DB foundation: read-scope schema, sample importer, local dev#9hatton wants to merge 17 commits into
Conversation
…al dev - supabase/migrations: initial read-scope schema (books/languages/tags/related_books/users + book_languages junction), generated from the authoritative production schema dump; TEXT PKs preserving Parse objectIds; RLS with anonymous public read - packages/sync-tool: idempotent sample importer pulling ~100 diverse production books (SYNC_* env vars, refuses non-localhost targets by default) - Local ports moved 543xx -> 443xx: Windows Hyper-V/WSL excluded port ranges silently blackholed the Supabase defaults (see README) - README: Podman-on-Windows setup (rootful machine, -x logflare,vector) replacing the Docker Desktop requirement - docs/db: schema conventions, corrections to the earlier draft plans, roadmap - ci.yml: validate migrations apply from scratch on PRs Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parse supported wildcard tag filters (e.g. bookshelf:X*) via Mongo regex on array elements; PostgREST has no per-element pattern operator. tags_text renders the array as |tag1|tag2|...| so LIKE can anchor on element boundaries. Includes an immutable array_to_string wrapper (required for generated columns). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base merge renamed MIGRATION-PLAN.md -> FUNCTIONS-MIGRATION-PLAN.md (develop commit 41f5a47); update the db README's cross-reference so the link resolves. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
| Filename | Overview |
|---|---|
| supabase/migrations/20260717120000_initial_read_schema.sql | Full read-scope schema: tables, indexes, and RLS policies for books/languages/tags/users/book_languages/related_books. RLS soft-delete guards on all junction tables are correctly applied. |
| supabase/migrations/20260717230000_add_books_tags_text.sql | Adds tags_text generated column using an IMMUTABLE wrapper around array_to_string to enable per-element wildcard tag matching via LIKE. Approach is well-justified by inline comments. |
| supabase/migrations/20260718000000_add_match_topic_tags_rpc.sql | Adds match_topic_tags RPC: STABLE, SECURITY INVOKER, pinned search_path; explicitly revokes PUBLIC and re-grants to anon/authenticated. Logic faithfully replicates Parse's topic-filter semantics without regex surface. |
| packages/sync-tool/src/import-sample.mjs | Idempotent Parse→Supabase sample importer. Localhost guard prevents accidental remote writes. Parse expanded-pointer vs. stub distinction is handled correctly throughout. |
| .github/workflows/ci.yml | Adds a migration-validation job that starts a stripped-down local Supabase stack, runs db reset, and stops it. Supabase CLI version pinned to match package.json. |
| supabase/functions/send-concern-email/concernEmail.ts | Migrates sendConcernEmail Parse cloud function to a Supabase Edge Function. Input validation, Mailgun integration, and no-op fallback are all well-tested. The fromAddress-from-body trust issue is acknowledged and annotated as a TODO. |
| supabase/functions/tests/send-concern-email-test.ts | Comprehensive tests covering validation, book-not-found, no-op env behavior, Mailgun request shape, fallback field values, error responses, and CORS headers. |
| supabase/config.toml | Ports moved from 543xx to 443xx to avoid Windows Hyper-V excluded ranges. New env secrets for Mailgun wired up. verify_jwt = false on send-concern-email is documented with a TODO for post-auth-cutover reversal. |
Reviews (7): Last reviewed commit: "Document the 200k-books performance desi..." | Re-trigger Greptile
|
[Claude Opus 4.8] Consulted Devin on 2026-07-17, up to commit Devin found 1 Bug + 3 Investigate + 3 Informational. Each actionable finding is mirrored as its own inline review thread:
Informational items skipped (stale 543xx refs in fs/README, Parse tag maxLimit, duplicate langPointers). CI ( |
- Drop the updated_at triggers (and handle_updated_at). An update trigger
stamping now() overwrote Parse's real timestamps on every importer
re-run (upsert update path). Verified: updated_at is now identical
across two back-to-back imports. Triggers return together with the
sync-gating mechanism (docs/db/README.md correction 7).
- Gate the books public-read policy with NOT is_deleted so future
tombstones are never served. No behavior change today (all rows false).
- Tighten the users public-read policy: a row is readable only while the
user has at least one visible book. Uploader emails stay embeddable
for book display (matches bloomlibrary.org), but the table is no
longer a listable email directory.
- Drop the legacy books.languages column instead of porting it.
Production evidence: only 146 books carry the field, none created
after Jan 2015, every value is []. Dropping it frees the name so the
documented embed select("*, languages(*)") resolves naturally through
book_languages (verified against the local stack).
- Make toSnake capital-run-aware (bloomPUBVersion -> bloom_pub_version),
removing the NAME_OVERRIDES special case. Verified every books field
in the authoritative Parse schema dump maps to a real column.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Greptile P1 follow-up to the is_deleted gate: junction rows (and related_books rows with no visible book left) must not enumerate the ids of soft-deleted books. Verified against the local stack: junction rows disappear from anon when their book is tombstoned, service role still sees them, and the languages(*) embed is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the import
Devin caught that a book listing the same language twice would put two
identical (book_id, language_id) rows into one upsert statement, which
Postgres rejects ("ON CONFLICT DO UPDATE command cannot affect row a
second time"), aborting the whole sample import. Reproduced the failure
against the local stack and verified the deduped import still produces
identical junction rows.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
[Claude Fable 5] Consulted Devin on 2026-07-18 up to commit 7990f7b. Two review cycles this run: the first (at 667563a) raised one Bug — duplicate langPointers aborting the sample import — fixed in 7990f7b (thread), and one Investigate about CI's supabase-start exclusion list, verified not an issue (thread). The re-review at 7990f7b marks all Bugs resolved with no new findings. CI and Greptile are green and quiet for the same commit. |
send-concern-email needs them for its Mailgun template; Parse already returns them on every book record, they just weren't typed yet. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the legacy Parse cloud function sendConcernEmail (bloom-parse-server,
cloud/emails.js) - blorg's "report a concern about this book" form. POST JSON
{ fromAddress, content, bookId }: looks up the book via BloomParseServer (the
same pattern fs already uses; there's no Supabase books table on develop yet)
and emails the report to the Bloom team via Mailgun's REST API directly
(no mailgun-js - it's deprecated), using the "report-a-book" template plus a
plain-text fallback body assembled from the same variables.
verify_jwt is false, same as fs and social: blorg still authenticates via
Parse during the transition, so there's no Supabase JWT to check yet.
Preserves the legacy no-op behavior when MAILGUN_API_KEY or
EMAIL_REPORT_BOOK_RECIPIENT is unset (used by test environments), and adds
input validation legacy never had (bookId shape, content length, email shape)
returning 400s instead of relying on downstream failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers validation failures (bad bookId/content/fromAddress, malformed JSON), unknown book (404), method/CORS handling, the no-op path when either Mailgun env var is unset, a Mailgun failure surfacing as 500, and the happy path - asserting the exact form fields and X-Mailgun-Variables JSON sent to Mailgun, plus the "unknown X" fallback text for missing book fields. Mocks BloomParseServer's book lookup and global fetch, same pattern as fs-handler-test.ts; no real secrets required. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
It's a Parse Cloud Code migration, not an Azure Function, so it doesn't fit the existing phases table; added a note under "This repo (Supabase)" instead, including the not-yet-merged parse-to-supabase-db-foundation branch's books table as a future candidate for this function's book lookup. Spec'd in api-spec-next.yml (not api-spec.yml) since it's implemented but not deployed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each sampler category had a hardcoded limit summing to ~123 books, so SAMPLE_TARGET only moved the early-exit threshold and could never grow the sample. Per-category limits now scale by SAMPLE_TARGET/100; used to build the 699-book local dataset behind blorg's integration suite. Also adds PAPERCUTS.md (deno --env-file hard-fails without .env.local). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per decision 2026-07-18: one backend PR. Brings the send-concern-email Supabase edge function (replacing the Parse sendConcernEmail cloud function) with its 15 tests, config, docs, and api-spec-next entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Parse resolved `topic:` filter values that aren't in kTopicList by running a case-insensitive regex over the tag array; the Supabase query builder had no per-array-element regex operator and silently dropped them, so an off-list topic showed an empty shelf where Parse showed books (read-parity gap A2). This adds public.match_topic_tags(topic_names text[]), which the blorg client calls to resolve non-canonical topic values to matching tag names, then requires "any of" those tags (overlaps). It mirrors Parse's semantics: a single value is an anchored ^topic:value$ case-insensitive match; two or more are unanchored topic:value substring matches OR-ed together. Security: STABLE, read-only, SECURITY INVOKER (no RLS bypass; reads only the public-read tags vocabulary and returns no book ids), pinned search_path, EXECUTE scoped to anon/authenticated/service_role. Values are matched as literals (equality / strpos), deliberately not honoring processRegExp's `/.../` raw-regex escape hatch, to avoid a server-side ReDoS surface. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Devin caught that [edge_runtime.secrets] only forwarded the Parse app ids, so MAILGUN_API_KEY and EMAIL_REPORT_BOOK_RECIPIENT from .env.local never reached the function locally - the concern-email path silently no-opped even for a developer who configured it. Verified supabase start still comes up cleanly when the vars are unset (they stay optional). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
[Claude Fable 5] Consulted Devin on 2026-07-18 up to commit be6db75 (re-run after the send-concern-email / match_topic_tags / SAMPLE_TARGET batch landed). One new Bug: the Mailgun secrets were never wired into the local edge runtime, so the concern-email flow silently no-opped in local dev — fixed in be6db75 (thread). No other new findings; CI and Greptile green and quiet for the same commit. |
Read-path decisions must assume 200k books (8x today's ~25k), so "small table, seq scan is fine" reasoning no longer applies to books. Fuzzy or wildcard matching on vocabularies (tags, topics, bookshelves, branding) resolves against the small vocabulary tables first and hits books only with exact GIN-indexed containment (the match_topic_tags pattern); the blorg wildcard-tag LIKE on tags_text should eventually migrate to that shape too. The one books-side index this calls for is a pg_trgm GIN index on the free-text search column, the common query that has no vocabulary to resolve against. Rare facets stay accepted seq scans. Prompted by reviewing Hatchet's "startup's Postgres survival guide" and its HN thread against this schema. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
[Claude Opus 4.8] PR opened by preflight.
What this establishes
The database read foundation for the Parse/MongoDB → Supabase Postgres migration. This is the durable artifact of the milestone (formerly branded "Local Supabase + Blorg"); the local-dev tooling is the scaffolding around it.
books/languages/tags/related_books/users+ thebook_languagesjunction, generated from the authoritative production Parse schema dump. TEXT PKs preserve Parse objectIds; RLS enabled with anonymous public read;books.tags_textgenerated column for wildcard tag matching.packages/sync-tool— an idempotent sample importer that pulls ~100 diverse production books into a local stack (refuses non-localhost targets by default).Parse remains the production system of record; nothing here serves production traffic yet. See
docs/db/README.mdfor schema conventions and the post-milestone roadmap.Notes
develop) integrated; itsMIGRATION-PLAN.md → FUNCTIONS-MIGRATION-PLAN.mdrename folded in and the db README cross-reference updated.