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.

SCHOOL IDENTITY students 8,051 rows 228 inbound FKs student_classes 8,473 purchases 6,059 campus_profiles 1,627 emails 39,869 + 224 more tables all hold real data VESTIGIAL CAMPUS IDENTITY users 27 rows 13 inbound FKs rooms 0 lecture_sessions 0 placed_objects 0 npc_conversations 0 user_matrix_memberships 0 all empty no foreign key joins these two tables campus_profiles.student_id is the only bridge, and it points left
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.

Stripe source of truth Platform webhook real time · dedup on charge_id no membership, no calendar Connected webhook Stripe Connect · dedup on payment_intent + account Batch, every ~10 min 17 phases · does everything blind to Connect purchases 6,059 · 14 job writers students 8,051 · 22 job writers student_classes 8,473 · 18 job writers classes 135 · 33 job readers classtimes 387 sessions class_id 1 : many tracking flags on student_classes are the only thing preventing duplicate emails, calendar invites and receipts across all three paths
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.

students 8,051 PK id int email varchar name · timezone admin · faculty · researcher bool membership_level · support_tier scholarship · suspended bool authentication_token · expires … 109 columns in total student_classes 8,473 PK id bigint FK student_id · class_id enrolled_at · enrollment_type enrollment_email_sent pre/post_class_email_sent google_calendar_invite_sent feedback_request_sent scholarship_participation_funded classes 135 PK id int name · description curriculum_slug ← access control stripe_product_id max_students soft cap registration_email_template hedgedoc_main_id … 37 columns in total purchases 6,059 PK purchase_id int FK student_id · class_id customer_email varchar charge_id ← path 1 dedup payment_intent_id + stripe_account_id ← path 2 dedup price · payment_date · receipt_url is_scholarship_donation bool classtimes 387 PK id int FK class_id start_time · end_time timestamptz, but Pacific wall-clock stored as +00 google_calendar_event_id workshop_link · homework stripe_payment_link 1 : many many : 1 1 : many 1 : many class_id — but a Stripe product is reused across cohorts, so the routing must be by payment DATE, not product amber = a field that has caused a production incident red = the three different dedup keys that do not see each other There is no unique constraint spanning (student_id, class_id) without a predicate — a partial index, which is why ON CONFLICT must repeat it or silently miss.
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.

enrollment_attempt_log2,030,224 pet_social_log1,668,121 matches345,5351 agent_activity_log221,102 agent_token_usage79,068 world_observations77,802 emails43,498 enrollment_events37,9451 agent_tool_executions35,906 experiment_events35,394 page_visits26,114 player_animations22,725 agent_conversation_participants17,846 inbound_emails15,0003 agent_memory_recent14,096 email_classifications12,540 skill_library_executions11,9741 curriculum_access_log11,354 student_classes8,473 agent_to_agent_conversations8,2022 students8,051228 incomplete_tasks8,0421 person_tags6,636 onboarding_room_invites6,544 news_queue6,084 purchases6,0592 nudge_log5,334 auth_audit_log3,950 form_submissions2,9144 student_cohort_assignments2,872 user_inventory2,5612 plant_forage_log2,554 submission_resources2,486 mutual_aid_resources1,9133 campus_profiles1,627 agent_approach_log1,613 animation_credits1,514 player_settings1,455 player_profiles1,364 student_interests1,175 fishing_streaks1,106 curriculum_pages1,0483 agent_conversations9431 agent_item_pool814 player_approach_prefs760 hackers_reminder_sends750 visitor_identity562 class_attendance475 friendship_hall_notes4631 email_verifications422 onboarding_introductions416 schema_migrations402 classtimes3876 agent_relationship_arcs386 class_scholarship_slot_ledger380 spark_transactions373 alignment_orgs364 agent_memory_moments349 moderation_audit_log346 sprite_comparisons345 ai_risk_cases344 testimonials324 item_grant_log263 item_templates25213 player_inventory2492 pet_digest_queue240 tags2391 player_quests236 standards2341 buildings2339 form_submission_fallback232 avatar_pool213 real_issues_sources198 pages194 school_schema_migrations185 ai_risk_markers180 class_pricing172 agent_relationships170 site_taglines1681 submission_triage161 merch_backgrounds1501 merch_analytics149 regions1446 aid_group_invites141 sprite_candidates1385 plant_discoveries136 plants13610 classes13518 products135 fishing_catches131 invite_tokens131 classes_syllabus_md_backup_20260725130 canonical_tags124 refresh_tokens116 associated_student_emails112 skill_nodes1081 store_items1057 backup_agentic_move_purch_20260602102 backup_agentic_move_sc_20260602102 research_library98 dorm_buildings923 student_cohorts912 tracks_classes85 error_issue_map81 vehicle_templates802 building_open_rooms77 matchmaking_preferences74 skill_library_skills734 ai_accountability_registry67 agent_promises61 greenway_flowers54 pack_resources54 real_issues533 npc_item_pool50 tracks504 student_room_profile48 matrix_bot_state45 need_area_cards452 cowork_seats441 plant_species445 prompt_responses44 shared_links44 pet_feeding_log41 real_issues_rebuttals40 agent_schedules39 drop_in_program_sessions36 animal_species343 db_assistant_conversations33 housing_progression33 card_candidates32 class_feedback32 ctf_submissions32 npc_schedules31 ctf_bots29 meetup_rsvps28 users2713 links26 mailing_lists26 animation_catalog254 class_memory25 merch_font_styles251 store_purchases252 agent_time_context23 agents2364 call_recordings23 dorm_assignments23 sprite_batches234 membership_products222 consumable_catalog202 elevator_scenes20 pending_associated_emails19 spark_balances18 agent_movement_config17 player_cosmetics17 project_members17 agent_capabilities16 campus_pets163 class_slot_suggestions16 moderation_mutes16 player_facts16 quest_templates163 store_item_spark_prices16 pet_offline_state15 resource_engagements15 solarpunk_submissions15 student_tracks15 agent_models141 npc_models14 risk_scoreboard14 subject_rooms14 agent_npcs13 holidays13 mutual_aid_posts13 npc_movement_config13 student_meetups122 approach_tuning_log11 greenway_zones11 accountability_map10 aid_groups101 student_class_withdrawals10 teachers105 skills_volunteers9 agent_checkins81 conservatory_zones81 course_quest_templates8 job_search_activities81 resource_packs81 skill_trees82 db_api_tokens7 forms71 pending_deletions7 platform_costs7 projects71 ta_mentorship_volunteers7 user_matrix_accounts7 agent_jobs6 agent_reward_pool6 audiences62 bounties61 llm_providers6 merch_products61 npc_jobs6 nudge_preferences6 project_roles61 teacher_applications6 ai_risk_submissions52 approach_config5 bounty_claims51 team_compensation5 agent_matrix_accounts4 community_contributors43 custom_item_requests4 friendship_hall_hearts4 npc_relationships4 programs4 site_settings4 support_tiers41 agent_shifts32 course_agent_templates32 course_npc_templates3 housing_zones31 membership_onboarding_sequences3 membership_tiers31 room_suggestion_votes3 room_suggestions31 weekly_schedule_sections3 agent_evolution2 agent_evolution_log2 agent_memory_longterm2 agent_plant_rewards2 agent_vacation2 campus_map_layers2 community_highlights2 custom_tuition_links2 drop_in_programs22 email_templates22 houses2 lessons2 match_intros2 modules21 npc_plant_rewards2 npc_room_assignments2 sponsors2 workbench_campaigns2 agent_room_assignments1 capstone_events1 curriculum_changes1 curriculum_maintenance1 instructor_tracks1 job_postings1 password_reset_tokens1 skill_library_agent_skills1 space_connections1 student_admin_notes1 transparency_reports1 world_season1 active_buffs0 activity_submissions0 activity_templates01 admin_notifications0 agent_archived_identities0 agent_companion_log0 agent_companions01 agent_dm_allowance0 agent_dm_preferences0 agent_extraction_queue0 agent_faculty_reports0 agent_inventory01 agent_npc_tool_executions0 agent_personality_additions0 agent_rag_usage0 agent_reward_log0 agent_student_blocks0 agent_vacation_memories0 agent_vacation_requests01 ai_tutor_cache0 animal_discoveries0 animal_friendships0 animal_generation_history0 animation_credit_transactions0 applications0 artifact_installations0 baba_yaga_quiz_results0 blocked_form_emails0 bounty_reputation0 building_classes0 bulletin_boards0 claimable_rewards0 class_audiences0 class_prerequisites0 class_times0 classtime_curriculum0 community_resource_submissions0 content_reports0 content_sync_jobs0 cost_reconciliation0 course_agent_instances0 course_npc_instances0 cowork_occupancy0 curriculum_images0 curriculum_page_versions0 custom_agents0 custom_npcs0 dj_sessions0 dorm_call_log0 dorm_election_candidates0 dorm_election_votes0 dorm_elections02 drop_in_program_breaks0 emote_bindings0 entity_skill_unlocks0 exercise_awareness0 exercise_cache01 exercise_feedback_passive0 exercise_submissions01 exercises02 experience_rooms01 experiment_decisions0 extraction_rate_limits0 fact_extraction_queue0 fast_travel_destinations01 feature_prefabs0 financial_goals0 garden_members0 garden_plots0 gem_balances0 gem_bounties01 gem_bounty_work_log0 gem_subscriptions0 gem_transactions0 generated_animation_sprites0 gmail_connected_accounts01 help_requests0 housing_properties0 infrastructure_donations0 introduction_outcomes0 island_zone_progress0 item_access_grants0 item_enchantments0 item_sprite_overlays0 lecture_answers0 lecture_contexts0 lecture_notes0 lecture_questions01 lecture_recordings0 lecture_sessions06 lecture_slides0 live_activities01 live_page_versions0 maps02 matrix_pending_verifications0 meeting_invitations0 meeting_reservations01 meeting_rooms01 meetup_attendance0 membership_gifts0 memberships01 merch_orders0 movie_screenings0 movie_theaters01 mutual_aid_volunteers0 npc_activity_log0 npc_approach_log0 npc_archived_identities0 npc_companion_log0 npc_companions0 npc_conversations0 npc_evolution0 npc_evolution_log0 npc_extraction_queue0 npc_inventory0 npc_matrix_accounts0 npc_memory_longterm0 npc_memory_moments0 npc_memory_recent0 npc_personality_additions0 npc_promises0 npc_rag_usage0 npc_relationship_arcs0 npc_time_context0 npc_to_npc_conversations0 npc_token_usage0 open_source_sprites0 organizations01 other_income0 patronage_items0 pending_item_gifts0 person_notes0 pet_sitting0 phishing_examples0 pixellab_spend0 placed_items0 placed_lights0 placed_objects0 plant_care_log0 plant_harvest_log0 plant_vacations0 plant_verifications0 planted_seeds0 player_consumables0 player_learning_progress0 player_vehicles0 portal_guns0 portal_traversals0 portals01 project_gardens04 project_plants02 project_reports0 proximity_matrix_rooms0 reaction_animation_mappings0 recapturable_sprites01 receipts0 research_weekly_log0 resource_flags0 rooms0 scheduled_jobs0 seating_history0 security_audit_log0 silly_walk_credits0 silly_walk_sprites0 skill_library_reviews0 skill_tree_assignments0 sponsored_items0 standup_streaks0 student_blocks0 student_connections01 student_discovered_destinations0 student_email_queue0 student_organizations0 student_profile_cache0 student_timeouts0 submission_comments0 submission_feedback0 submission_standards0 submissions02 td_apt_cohorts02 td_apt_members0 td_augmentations0 td_banners0 td_capybara_events0 td_ctf_teams0 td_cve_inventory0 td_hack_scripts0 td_loot_inventory0 td_participants0 td_recovery_missions0 td_rewards0 td_security_events0 td_sessions012 td_student_records0 td_terminal_hacks0 td_wave_log0 teacher_stripe_accounts01 terms_consent0 topic_suppression0 trade_offers0 user_matrix_memberships0 user_reports0 vehicle_sprite_queue0 vending_machines0 wild_animal_instances0 wild_plant_state0 zeppelin_sessions0

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.

TableRowsJob writersJob readersConcentration
students8,0512224
student_classes8,4731814
purchases6,0591413
classes135933
classtimes387513
class_pricing37
mutual_aid_resources1,91331
enrollment_events37,90220

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

JobWritesReads
get_purchases_by_productproductsStripe API
create_new_studentsstudentspurchases, associated_student_emails
register_students_from_purchases_tablestudent_classes, studentsclasses, purchases
member_levelsstudents, purchasesmembership_products, products
send_enrollment_emailsstudent_classesclasses, students
calendar_inviter_with_delegationstudent_classesclasses, classtimes, students
send_weekly_schedulestudentsclasses, classtimes, student_classes, links
enrollment webhookpurchases, students, student_classesclasses
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.pywrites 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.

FamilyTablesEmpty% emptyRows
Tower defense td_*17170
Matrix / chat / message5352
Store / economy / item26153,906
Quest / skill / exercise251312,467
Pets / plants / animals23121,671,339
World / room / building / map341585,086
Agents agent_*5217380,947
School core — students / classes43132,109,557
Mutual aid / community1134,487
Email / notification10270,365
Everything else21596480,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 tableInbound FKsWhat it silently disables
td_sessions12The entire tower-defense subsystem
lecture_sessions6Lecture capture and broadcast — six dependent tables
project_gardens4Project gardens; the tick job runs every 6 hours against nothing
maps2Server-side collision. isAreaBlocked always fails open — campus #944
exercises2Exercise quests can never complete — campus #941
submissions2Documented as non-functional in UNIMPLEMENTED_FEATURES.md
dorm_elections2Dorm governance
roomsClassrooms unreachable in the UI — campus #850. Keyed to users, not students
meeting_rooms · meeting_reservations1 eachRoom booking; the cleanup job runs every 5 minutes against nothing
gem_bounties1Gem bounties
movie_theaters1Movie theatres
agent_companions1Agent companions
building_classesClass 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

JobCron
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-triageevery 6 h
project-garden-tickevery 6 h — 0 rows
cost-reconciliationdaily 00:05
daily-engagementdaily 00:15
vacation-transitionsdaily 00:30
relationship-decaydaily 03:30
approach-preference-decaydaily 04:00
community-journaldaily 06:00
spark-weekly-stipendSun 00:00
economy-weekly-auditSun 01:00
approach-global-tuningSun 04:00
weekly-project-reportsMon 09:00
vacation-accrual1st 02:00
dm-allowance-reset1st 02:30
book-club-monthly-award1st 03:00
monthly-report1st 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
  • sync_calendar_events — reads classes, classtimes
  • update_business_metrics — refreshes Prometheus gauges
  • 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.