Перейти до основного вмісту

Database

The ChurchApps API uses a database-per-module architecture. Each of the six data modules has its own MySQL database with an independent connection pool, providing clear data boundaries while keeping everything within a single deployment.

Before You Begin

Architecture Overview

Api
├── membership_db ← People, groups, permissions
├── attendance_db ← Services, sessions, records
├── content_db ← Pages, sections, elements
├── giving_db ← Donations, funds, payments
├── messaging_db ← Conversations, notifications
└── doing_db ← Tasks, plans, assignments

Key Design Decisions

  • One database per module -- Each module maintains its own MySQL database with a dedicated connection pool (managed by KyselyPool). This keeps modules decoupled and allows independent schema evolution.
  • Exclusive ownership -- A module's tables are read and written only by that module's own code. When another module needs the data, it calls the owning module's gateway rather than querying the tables itself -- see Cross-Module Communication.
  • Repository pattern without an ORM -- All data access goes through repository classes that build typed SQL with the Kysely query builder against the module's schema. This gives full control over query performance and behavior.
  • Multi-tenant by design -- Every query is scoped by churchId. All tables include a churchId column, and the repository layer enforces tenant isolation automatically.

Connection Strings

Each module's database connection is configured in .env using standard MySQL connection string format:

mysql://user:password@host:port/database

For example, a local development setup might look like:

Each module reads its connection from an environment variable named <MODULE>_CONNECTION_STRING:

MEMBERSHIP_CONNECTION_STRING=mysql://root:password@localhost:3306/churchapps_membership
ATTENDANCE_CONNECTION_STRING=mysql://root:password@localhost:3306/churchapps_attendance
CONTENT_CONNECTION_STRING=mysql://root:password@localhost:3306/churchapps_content
GIVING_CONNECTION_STRING=mysql://root:password@localhost:3306/churchapps_giving
MESSAGING_CONNECTION_STRING=mysql://root:password@localhost:3306/churchapps_messaging
DOING_CONNECTION_STRING=mysql://root:password@localhost:3306/churchapps_doing
інформація

In production, connection strings are stored in AWS SSM Parameter Store and read by the Environment class at startup.

Schema Scripts

Table schemas are defined as Kysely migrations in the tools/migrations/ directory, organized by module:

tools/migrations/
├── membership/
├── attendance/
├── content/
├── giving/
├── messaging/
└── doing/

Migrations define table creation, indexes, and schema changes. The tools/dbScripts/ directory holds demo and seed data that can be loaded on top of the schema.

Database Initialization

Initialize all databases

npm run initdb

This creates all six databases and runs the migrations for each one.

Initialize a single module

npm run initdb -- --module=membership
поради

When working on a specific module, you can re-initialize just that module's database without affecting the others.

Data Access Pattern

Repositories build queries with the Kysely query builder against the module's typed database schema, obtained through the module's getDb() function. A typical repository method looks like this:

public async loadAll(churchId: string) {
return getDb().selectFrom("people").selectAll()
.where("churchId", "=", churchId)
.execute();
}

Repositories are obtained via RepoManager:

const repos = await RepoManager.getRepos<Repos>("membership");
const people = await repos.person.loadAll(churchId);
попередження

Always include churchId in your queries to maintain multi-tenant isolation. Never query across tenants unless you have a specific, authorized reason to do so.

Cross-Module References

Because each module's data lives in a separate database, there are no foreign keys or SQL joins across module boundaries. A record that relates to another module's data stores that record's id -- for example, a donation in the giving database carries the personId of a person in the membership database -- and any cross-module composition happens in application code.

This constraint is what makes the module boundaries real: each schema can evolve independently, a module's database can be moved to its own server, and a module could even be extracted into a standalone service without untangling shared tables or cross-database queries.