Multiverse SchoolData Mapread live from prod, 2026-08-06
463 tables. 207 of them are empty.
School and campus share one PostgreSQL database. This is every table in it, what points at what, which jobs write where — and the structural reason why almost half the schema has never held a row.
Public copy. This page is readable by anyone with the link. Infrastructure addresses and the specifics of unresolved credential items have been removed; every number, finding and roadmap item is unchanged. The complete version lives behind the admin gate at /internal/decks.
463
Tables
One database, both products
4.8M
Rows total
42% of them in two log tables
207
Empty tables
45% of the schema
559
Foreign keys
228 of them point at one table
Finding 1the root cause
There are two identity tables, and the wrong one has the features
Campus was built with its own users table. The unification merged the databases but not the identities. Today users holds 27 rows and students holds 8,051 — and a dozen campus features are still keyed to users.
The mechanism behind a dozen open issues. A feature written against users can only ever see 27 people, so it looks broken to all 8,051. rooms being empty is why classrooms are unreachable (campus #850); maps being empty is why server-side collision fails open (#944); lecture_sessions being empty has six other tables hanging off it. These were filed as seven separate bugs. They are one decision.
Finding 2the money path
The enrollment core, and the three doors into it
Five tables carry the entire commercial life of the school. Three independent code paths write to them, and two of the three cannot see what the other did.
Why this system has had four multi-month silent failures. The three paths use three different dedup keys, so cross-path deduplication is not guaranteed. Membership updates, calendar invites, receipts and the weekly digest happen only in the batch path — if the batch stops, students still get enrolled and silently receive none of it. And Stripe Connect has no batch safety net at all.
ReferenceDDL
The five tables, column by column
The actual columns, as they exist in production today. Nearly every enrollment bug in the repo's history lives somewhere in this picture.
Everything that has gone wrong lives in the coloured fields. The four _sent booleans on student_classes are the only thing standing between a student and a duplicate welcome email from three different code paths. The two dedup keys on purchases belong to two different webhooks that cannot see each other's rows. And classtimes stores Pacific wall-clock time labelled +00, so any raw > now() comparison is seven to eight hours early.
Referenceeverything
All 463 tables
Sorted by row count. Cyan is the row count; magenta ←n is the number of foreign keys pointing at it. Dashed and faded means zero rows — 207 of them.
Scroll it. The shape is the argument: a short dense head of real systems, then a very long tail of schema that was designed, migrated, foreign-keyed and never filled.
Finding 3contention
Twenty-two jobs write to students
Every job file in the repo was scanned for INSERT, UPDATE and DELETE targets. 89 job files touch 45 tables. The distribution is not even.
Table
Rows
Job writers
Job readers
Concentration
students
8,051
22
24
student_classes
8,473
18
14
purchases
6,059
14
13
classes
135
9
33
classtimes
387
5
13
class_pricing
—
3
7
mutual_aid_resources
1,913
3
1
enrollment_events
37,902
2
0
Eleven tables are written by exactly one job and read by none — curriculum_pages, platform_costs, job_postings, enrollment_attempt_log, solarpunk_submissions and others. Those are the healthy ones: a single owner, a clear contract.
The live enrollment chain, in order
Job
Writes
Reads
get_purchases_by_product
products
Stripe API
create_new_students
students
purchases, associated_student_emails
register_students_from_purchases_table
student_classes, students
classes, purchases
member_levels
students, purchases
membership_products, products
send_enrollment_emails
student_classes
classes, students
calendar_inviter_with_delegation
student_classes
classes, classtimes, students
send_weekly_schedule
students
classes, classtimes, student_classes, links
enrollmentwebhook
purchases, students, student_classes
classes
Finding 4hazard
Seven abandoned enrollment writers are still in the repo
Alongside the live path, the jobs directory contains seven other programs that write to students, student_classes and purchases. Two of them are named _broken.
The enroll_students_from_product_dict family
enroll_students_from_product_dict.py
enroll_students_from_product_dict_robust.py
enroll_students_from_product_dict_broken.py
enroll_students_from_product_dict_broken2.py
All four write students and student_classes. Two announce in their filename that they do it wrong.
The new_enrollment_process family
new_enrollment_process.py
new_enrollment_process_enhanced.py
new_enrollment_process_complete.py — writes 5 tables, the widest blast radius of any job in the repo
Three generations of a rewrite that never replaced the original. "Complete" is not the one that runs.
Why this is a live risk, not just clutter. The job runner executes what Windmill tells it to, and Windmill's schedule lives outside the repo — so the repo cannot tell you which of these is wired up. Anyone reading the directory to understand enrollment has a one-in-eight chance of reading the right file, and an agent asked to "fix enrollment" has the same odds. This is the exact shape of the bug that broke webhooks for four months: a plausible-looking path that nobody was actually running.
Finding 5the shape
The schema by family
Grouped by name prefix and subject. The % empty column is the interesting one — it reads as a map of which ambitions got built and which got scaffolded.
Family
Tables
Empty
% empty
Rows
Tower defense td_*
17
17
0
Matrix / chat / message
5
3
52
Store / economy / item
26
15
3,906
Quest / skill / exercise
25
13
12,467
Pets / plants / animals
23
12
1,671,339
World / room / building / map
34
15
85,086
Agents agent_*
52
17
380,947
School core — students / classes
43
13
2,109,557
Mutual aid / community
11
3
4,487
Email / notification
10
2
70,365
Everything else
215
96
480,426
Where all the rows actually are
Two log tables hold 42% of the entire database: enrollment_attempt_log at 2,030,224 rows and pet_social_log at 1,668,121. Neither has a retention policy. Campus #1074 is about exactly this, and campus #978 explains the pet one — a quadratic proximity loop that wrote 1.2M rows in July and pins every pet at maximum happiness, so the mood toasts it exists to trigger can never fire.
The tower-defense subsystem is 17 for 17
Seventeen tables, every one empty, twelve foreign keys pointing at an empty td_sessions. A fully-normalised schema for a game that has never been played once. Worth a deliberate decision: finish it, or drop the tables and keep the spec.
Finding 6dead hubs
Thirty empty tables that other tables depend on
An empty leaf table is a feature nobody used. An empty table that other tables point at is a feature that was wired all the way through and then never switched on. Each of these is a subsystem waiting on one row.
Empty table
Inbound FKs
What it silently disables
td_sessions
12
The entire tower-defense subsystem
lecture_sessions
6
Lecture capture and broadcast — six dependent tables
project_gardens
4
Project gardens; the tick job runs every 6 hours against nothing
maps
2
Server-side collision.isAreaBlocked always fails open — campus #944
exercises
2
Exercise quests can never complete — campus #941
submissions
2
Documented as non-functional in UNIMPLEMENTED_FEATURES.md
dorm_elections
2
Dorm governance
rooms
—
Classrooms unreachable in the UI — campus #850. Keyed to users, not students
meeting_rooms · meeting_reservations
1 each
Room booking; the cleanup job runs every 5 minutes against nothing
gem_bounties
1
Gem bounties
movie_theaters
1
Movie theatres
agent_companions
1
Agent companions
building_classes
—
Class auto-placement on login has never fired — campus #929
The pattern is worth naming: none of these fail loudly. An empty table returns zero rows, which every caller reads as "nothing to do." That is the same failure mode as the four-month webhook outage and the three-month backup gap, expressed in the schema instead of the code.
Finding 7the clocks
What is running, and how often
Campus — pg-boss cron, exact
Job
Cron
plant-tick
*/5 — every 5 min
meeting-room-cleanup
*/5 — 0 rows
community-snapshot
*/5 — every 5 min
skill-scheduled-execution
*/15
vehicle-loan-cleanup
*/15
pet-decay-tick
*/30
pet-sitting-tick
:15 hourly
agent-gem-consumption
:00 hourly
hive-bug-triage
every 6 h
project-garden-tick
every 6 h — 0 rows
cost-reconciliation
daily 00:05
daily-engagement
daily 00:15
vacation-transitions
daily 00:30
relationship-decay
daily 03:30
approach-preference-decay
daily 04:00
community-journal
daily 06:00
spark-weekly-stipend
Sun 00:00
economy-weekly-audit
Sun 01:00
approach-global-tuning
Sun 04:00
weekly-project-reports
Mon 09:00
vacation-accrual
1st 02:00
dm-allowance-reset
1st 02:30
book-club-monthly-award
1st 03:00
monthly-report
1st 01:00
Read from server/src/services/jobScheduler.ts. This one is legible because it is in the repo.
School — Windmill, not in the repo
The schedule is not version-controlled
School's job schedules live in Windmill (workspace multiverse), which drives job-runner:5050/run/<job>. The n8n JSON files in the repo are decoration and host cron is not used.
So the repo can tell you what a job would do and not whether it runs. Combined with the seven ghost enrollment writers, that means the single most important loop in the business — who gets enrolled and emailed — cannot be fully understood from the codebase.
What we know runs
full_enrollment_process — every ~10 min, 17 phases, reads purchases and orchestrates the rest
community_resource_review — on demand, via the campus RPC
school-quality-sweep daily 02:00, school-resource-health Sun 03:00, school-community-digest Mon 09:00 PT — these three are scheduled from campus pg-boss and dispatched back into school's job runner
The dispatch has no retry
School → campus RPC → pg-boss → back to job-runner. Campus #1116: a job-runner restart of a few seconds fails the whole pg-boss job, with nothing to pick it back up.
Conclusion
Four things this map argues for
1 · Resolve the identity fracture before fixing its symptoms
Campus #1040 already suspects "one candidate root cause behind #944, #990, #1004, #1005 and #1021." This is it. Either migrate the users-keyed tables onto students, or make users a real view over students. Fixing rooms, maps and lecture_sessions one at a time will keep working and keep not working.
2 · Delete the seven ghost enrollment writers
Two are named _broken. Deleting them is a pure subtraction that makes the most fragile system in the business legible for the first time. Confirm against Windmill which one is scheduled, keep that, remove the rest.
3 · Put retention on the two log tables
enrollment_attempt_log and pet_social_log are 42% of the database on a host that filled its disk four days ago and took Postgres down with it. Retention here is cheaper than another 500GB volume.
4 · Make an empty table an alert, not a shrug
Thirty subsystems are dark because zero rows reads as "nothing to do." A check that asserts this table should not be empty in production would have caught collision, class placement, classrooms and lecture capture — before a student found them. That is one query, on a schedule, and it is the cheapest thing on this page.
Method, so you can re-run it. Table list and row counts from a generated UNION ALL of count(*) over every table the school role can read. Foreign keys from pg_constraint where contype='f'. Job wiring by scanning all 89 SQL-touching files under jobs/ for FROM/JOIN versus INSERT INTO/UPDATE/DELETE FROM. Campus cron read from jobScheduler.ts.
One caveat worth knowing:pg_stat_user_tables is useless on this database right now — its counters were reset by the unclean shutdown on Aug 2, so it reports 421 tables as never-written including ones holding 345,535 rows. Every count here is a real count(*) for that reason.