Skip to content

Add Kysely database migrations for self-hosting#17

Open
stephen-dahl wants to merge 1 commit intoChurchApps:mainfrom
stephen-dahl:feature/kysely-migrations
Open

Add Kysely database migrations for self-hosting#17
stephen-dahl wants to merge 1 commit intoChurchApps:mainfrom
stephen-dahl:feature/kysely-migrations

Conversation

@stephen-dahl
Copy link

@stephen-dahl stephen-dahl commented Feb 7, 2026

The problem

Setting up or updating ChurchApps requires running raw SQL scripts through initdb.ts — a 600-line script that reads ~100 .sql files and executes them with no version tracking. There's no way to know what state a database is in, no way to roll back a bad schema change, and no way for self-hosters to apply incremental updates without re-reading the full init script source code.

Ad-hoc migration scripts have accumulated in scattered places (giving/migrations/, content/links_visibility_migration.sql, giving/eventLogs_migration.sql) with no unified way to run them.

What this does

Adds Kysely-managed migrations — one command to set up or update all 6 databases:

npm run migrate        # apply all pending migrations
npm run migrate:status # see what's been applied

Each database tracks its own migration history in a kysely_migration table. Kysely is already a MySQL query builder (no ORM overhead), weighs ~50KB, and has zero dependencies.

For self-hosters

Before: Read initdb.ts, understand the module system, hope your DB matches the SQL files.

After: npm run migrate — done. Every future schema change is a numbered migration file that runs exactly once.

For us (developers)

Before: Edit a .sql file and hope everyone re-runs initdb. No rollback. No version tracking.

After:

npm run migrate:create -- --module=membership --name=add_email_verified
# Edit the generated file, commit it, done.
# It runs automatically on next `npm run migrate`.

For existing deployments (prod/staging)

npm run migrate:baseline  # marks current schema as "already applied"
npm run migrate           # only runs new migrations going forward

What's included

File Purpose
tools/migrations/kysely-config.ts Creates a Kysely instance per module using existing Environment.getDatabaseConfig()
tools/migrate.ts CLI runner: --module=<name>, --action=up|down|status
tools/migrate-create.ts Scaffolds new migration files with date-prefixed names
tools/migrate-baseline.ts Marks initial migration as applied on existing databases
tools/seed.ts Loads demo data from existing demo.sql files
tools/migrations/<module>/2026-02-06_initial_schema.ts One per module (6 total) — all 96 tables

What didn't change

  • All existing SQL schema files in dbScripts/ are untouched (still the source of truth for reading)
  • initdb.ts still works, just prints a deprecation notice
  • No runtime code changes — this is purely a tooling addition
  • demo.sql files untouched (used by the new seed runner)

What was cleaned up

  • Removed unused 01_tables.sql draft files (had different schema than the actual .sql files)
  • Removed scattered ad-hoc migration scripts (giving/migrations/, eventLogs_migration.sql, links_visibility_migration.sql)

npm scripts added

migrate, migrate:up, migrate:down, migrate:status
migrate:create, migrate:baseline
migrate:membership, migrate:attendance, migrate:content
migrate:giving, migrate:messaging, migrate:doing
seed, seed:reset

Test results

All tests run against MySQL 8.0 in Docker (mysql:8.0 on port 3307).

Fresh database — all 96 tables created across 6 databases

Ensuring databases exist...
  ✓ coredb created/verified
  ✓ attendancedb created/verified
  ✓ contentdb created/verified
  ✓ givingdb created/verified
  ✓ messagingdb created/verified
  ✓ doingdb created/verified

Running migrations...
  [membership]  2026-02-06_initial_schema  ✓
  [attendance]  2026-02-06_initial_schema  ✓
  [content]     2026-02-06_initial_schema  ✓
  [giving]      2026-02-06_initial_schema  ✓
  [messaging]   2026-02-06_initial_schema  ✓
  [doing]       2026-02-06_initial_schema  ✓

Idempotent — second run is a no-op

Running migrations...
  [membership]  Already up to date.
  [attendance]  Already up to date.
  [content]     Already up to date.
  [giving]      Already up to date.
  [messaging]   Already up to date.
  [doing]       Already up to date.

Status check

  [membership]  2026-02-06_initial_schema  applied
  [attendance]  2026-02-06_initial_schema  applied
  [content]     2026-02-06_initial_schema  applied
  [giving]      2026-02-06_initial_schema  applied
  [messaging]   2026-02-06_initial_schema  applied
  [doing]       2026-02-06_initial_schema  applied

Down/Up roundtrip (attendance module)

  [attendance] Rolled back: 2026-02-06_initial_schema  ✓
  [attendance] 2026-02-06_initial_schema  ✓

Seed data

  [membership] Loaded demo data (10 statements)
  [attendance] Loaded demo data (6 statements)
  [content]    Loaded demo data (29 statements)
  [giving]     Loaded demo data (6 statements)
  [messaging]  No demo.sql found, skipping.
  [doing]      No demo.sql found, skipping.

Baseline (for existing databases)

Applying baseline to existing databases...
  [membership] Baseline applied — 2026-02-06_initial_schema marked as executed.
  [attendance] Baseline applied — 2026-02-06_initial_schema marked as executed.
  [content]    Baseline applied — 2026-02-06_initial_schema marked as executed.
  [giving]     Baseline applied — 2026-02-06_initial_schema marked as executed.
  [messaging]  Baseline applied — 2026-02-06_initial_schema marked as executed.
  [doing]      Baseline applied — 2026-02-06_initial_schema marked as executed.

Test plan

  • Fresh DB: npm run migrate on empty MySQL — all 96 tables created
  • Idempotent: run npm run migrate twice — second run is a no-op
  • Rollback: npm run migrate:down -- --module=attendance then migrate:up — tables recreated
  • Status: npm run migrate:status — shows applied migrations
  • Baseline: on a DB with existing tables, npm run migrate:baseline then npm run migrate — no-op
  • Seed: npm run seed after migrate — demo data loads
  • Rebased on latest main — picked up planItems.label varchar(100) + thumbnailUrl column

🤖 Generated with Claude Code

@stephen-dahl stephen-dahl force-pushed the feature/kysely-migrations branch 2 times, most recently from bc214d8 to 91d49a2 Compare February 7, 2026 19:42
Replace the ad-hoc SQL init scripts with a proper migration system using
Kysely. Each of the 6 databases (membership, attendance, content, giving,
messaging, doing) gets versioned, trackable migrations that can be run
forward or rolled back.

New commands:
  npm run migrate          — apply all pending migrations
  npm run migrate:status   — see what's applied vs pending
  npm run migrate:down     — roll back the last migration
  npm run migrate:create   — scaffold a new migration file
  npm run migrate:baseline — mark existing DBs as up-to-date
  npm run seed             — load demo data

Existing initdb.ts still works but prints a deprecation notice.
Removed unused 01_tables.sql drafts and old ad-hoc migration scripts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@stephen-dahl stephen-dahl force-pushed the feature/kysely-migrations branch from 91d49a2 to 122507c Compare February 7, 2026 19:48
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