Skip to content

Parse → Supabase DB foundation: read-scope schema, sample importer, local dev#9

Draft
hatton wants to merge 17 commits into
developfrom
parse-to-supabase-db-foundation
Draft

Parse → Supabase DB foundation: read-scope schema, sample importer, local dev#9
hatton wants to merge 17 commits into
developfrom
parse-to-supabase-db-foundation

Conversation

@hatton

@hatton hatton commented Jul 18, 2026

Copy link
Copy Markdown
Member

[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.

  • Read-scope schema as migrationsbooks / languages / tags / related_books / users + the book_languages junction, generated from the authoritative production Parse schema dump. TEXT PKs preserve Parse objectIds; RLS enabled with anonymous public read; books.tags_text generated 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).
  • Local dev — Podman-on-Windows setup, ports moved 543xx→443xx to dodge Windows Hyper-V excluded ranges, docs.
  • CI — validates the migrations apply cleanly from scratch on every PR.

Parse remains the production system of record; nothing here serves production traffic yet. See docs/db/README.md for schema conventions and the post-milestone roadmap.

Notes

  • Base (develop) integrated; its MIGRATION-PLAN.md → FUNCTIONS-MIGRATION-PLAN.md rename folded in and the db README cross-reference updated.

hatton and others added 5 commits July 17, 2026 16:12
…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>
@greptile-apps

greptile-apps Bot commented Jul 18, 2026

Copy link
Copy Markdown

Greptile Summary

This PR establishes the database read foundation for the Parse/MongoDB → Supabase Postgres migration: a versioned schema for books, languages, tags, users, and junction tables; an idempotent sample importer; and a migrated send-concern-email Edge Function. It also wires up CI migration validation and adjusts local ports for Windows Hyper-V compatibility.

  • Three SQL migrations define the read-scope schema with TEXT PKs (preserving Parse objectIds), GIN indexes, and RLS policies on every table — soft-delete guards are applied consistently across books, book_languages, related_books, and users.
  • packages/sync-tool pulls a diverse ~100-book sample from production Parse into the local stack; a localhost-only guard (overridable via SYNC_ALLOW_REMOTE=1) prevents accidental remote writes.
  • send-concern-email replaces the legacy Parse cloud function, with thorough input validation, a Mailgun no-op fallback when env vars are absent, and a full test suite; verify_jwt = false is acknowledged as a temporary trade-off with a TODO for the auth cutover.

Important Files Changed

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

Comment thread supabase/migrations/20260717120000_initial_read_schema.sql Outdated
Comment thread supabase/migrations/20260717120000_initial_read_schema.sql Outdated
Comment thread supabase/migrations/20260717120000_initial_read_schema.sql Outdated
Comment thread supabase/migrations/20260717120000_initial_read_schema.sql Outdated
Comment thread packages/sync-tool/src/import-sample.mjs
@hatton

hatton commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8] Consulted Devin on 2026-07-17, up to commit 41a9c9d.

Devin found 1 Bug + 3 Investigate + 3 Informational. Each actionable finding is mirrored as its own inline review thread:

  • Bugupdated_at trigger clobbers Parse timestamps on re-import → open, in the developer decision report.
  • Investigatelanguages embed alias collides with the legacy languages column → open, decision report.
  • Investigate — anonymous read exposes all users emails → open, decision report.
  • InvestigatetoSnake can drop capital-run analytics fields → assessed acceptable for the v0 logging importer, resolved (verify-at-full-sync noted).

Informational items skipped (stale 543xx refs in fs/README, Parse tag maxLimit, duplicate langPointers). CI (test) green — deno suite + migrations-apply-from-scratch. Greptile green with one P1 (soft-deleted books readable via using(true)), also in the decision report.

- 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>
Comment thread supabase/migrations/20260717120000_initial_read_schema.sql Outdated
hatton and others added 2 commits July 18, 2026 07:09
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>
Comment thread packages/sync-tool/src/import-sample.mjs
Comment thread .github/workflows/ci.yml
@hatton

hatton commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

[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.

hatton and others added 8 commits July 18, 2026 10:13
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>
Comment thread supabase/config.toml
@hatton

hatton commented Jul 18, 2026

Copy link
Copy Markdown
Member Author

[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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant