← back to journal
EngineeringJun 12, 20263 minpart 1 of 3 — the schema-sync saga

Every school thinks it's the only one. That's the whole point.

arc: firebase, one school, fine until it was not → layer it first so the swap lives in one place → multi-tenant → every schema change has to land in two places

when we started the school ERP we were on firebase. one school, one database, everything in one place. it was fine until it was not.

then marketing kicked in, real clients were about to come on board, and we decided to move from firebase to postgres.

but the backend had no real layering yet. so before the migration Jagan sir made us do separation of concerns properly first, splitting everything into routes, controllers, services, repositories and adapters. i spent days on it, making that flow concrete across the whole backend.

the payoff was the entire reason we did it:

servicerepositoryadapterthe only filethat knows thedatabasefirebasepostgresanything
swap what sits under the adapter and nothing above it changes — the service never learns which database answered.

good layering is not about tidy folders. it is so that the thing you will swap one day lives in exactly one place. we swapped the adapter, moved to postgres, and shipped the ERP.

then the real shape of the problem showed up. school switching. one school might run several branches, another has none, and lots of different schools were about to share the same portal. on a single-tenant design that is a data nightmare waiting to happen. one wrong query and school A sees school B's students.

this is where Jagan sir and his friend Surendra, a backend dev, sat down and designed our way out. go multi-tenant.

one postgres databasepublicthe tenant registrytenant_schoolAits own tablestenant_schoolBits own tablesno query can cross
isolation is the schema boundary itself, not a `where school_id =` clause somebody has to remember to write.

register a new school, a new tenant gets created, and it gets its own world. that one decision turned a scary shared database into every school behaving like it is the only one on the platform. it was a big revamp and it was worth every minute.

then came the part nobody warns you about. keeping every tenant's schema in sync. each new school needs all the tables. every existing school needs every future change. so we split migrations into two:

base-schema/the whole structurerun oncea new schoolborn with every tableversions/each change, numberedreplayedexisting schoolscatch up, one by oneevery school has to end up at the same schema
one folder births a tenant, the other keeps the tenants that already exist from drifting apart.

new school, run the base schema, it is born with everything. existing schools need a new column, write a numbered migration and run it across them.

and here is the rule that bites you the moment you forget it. every schema change has to land in both places. the numbered migration so existing schools get it, and the base schema so the next new school is born with it. update only one and your tenants quietly drift apart, new schools missing a column or old schools missing it.

this is exactly where AI trips. ask claude to add a column to the teachers table and it writes you a clean migration and forgets the base schema entirely. now every newly created school is missing that column, and you do not find out until one of them breaks.

so now, every time claude touches the schema, the question is automatic. did you update both, the versioned migration and the base template. i do not merge until it says yes and i have seen both diffs.

a migration that updates one of two sources of truth is not a migration. it is a future bug with a version number.

firebase to postgres, single-tenant to multi-tenant, one loose migration to a disciplined two-folder system. none of it was the feature the client asked for. all of it is why the feature keeps working as we onboard the next school.

#buildinpublic #softwareengineering #postgres #multitenancy #databasemigrations #maahitatechnologies

Send this as proof →Share on LinkedIn