Multi-campus engagement platform
Built for Thrive / OLAMI
Four campuses, one database, isolation that had to be provably real.
A network of campus organisations ran on a 135,000-line WordPress plugin. One install per campus, student records duplicated across sites, no shared reporting, and a Salesforce org nobody trusted. We replaced it with a single multi-tenant platform.
Same query. Two database roles.
The application never changed. Only the credential it connects with did.
-- policy: students.org_id = current_setting('app.current_org_id') -- table has ENABLE + FORCE ROW LEVEL SECURITY SELECT name, campus FROM students LIMIT 8;
thrive
rolsuper true
rows 8
The managed Postgres made the app’s user the cluster’s bootstrap superuser, and superusers bypass row-level security unconditionally. FORCE ROW LEVEL SECURITY does not apply to them. Every policy was correct. Nothing errored. Isolation was simply off. The bootstrap user cannot be demoted, so the fix was a second role owning every table.
The problem
- One WordPress install per campus: the same student existing three times with three histories.
- No way to answer any question across the network without exporting three spreadsheets.
- Salesforce was the system of record, fed by manual weekly CSVs that were stale on arrival.
What we built
- A single Laravel + Filament platform on one Postgres database, with per-campus tenancy enforced by row-level security.
- A per-campus admin panel, plus a cross-campus HQ panel for the parent organisation. The same data, two scopes, one permission model.
- A drag-and-drop public site builder so each campus runs its own public-facing site out of the same platform.
- Two-way Salesforce sync built on bridge objects, with flow deployment via the Metadata API.
Outcome
- Four campuses live on one platform; the legacy plugin retired.
- Salesforce syncing twice daily across all campuses, zero failures and zero duplicate contacts on cutover.
- The mobile app moved onto the new backend without a forced update.
- Cross-network reporting that previously required three exports and a spreadsheet is now a page.
What something like this costs
It took 59 working days, counted out of the repository rather than estimated afterwards.
A rate comparison, not a bill. Our figure is the published ladder from the pricing page, for the closest match to this shape of work. Working days regenerate from the repository on every deploy.
The full write-up problem, constraints, and the whole debugging story. About a 4-minute read
01 The problem
Each campus ran its own copy of a large WordPress plugin. The same student could exist three times across three installs with three different histories, and there was no way to answer a question across the network. Headcount, attendance, engagement: without exporting three spreadsheets and reconciling them by hand.
The parent organisation treated Salesforce as the system of record, but the data reaching it came from manual weekly CSV exports. By the time anyone looked, it was stale and partly wrong.
02 Constraints
- Real student PII across every campus: the isolation story had to be provable, not asserted.
- The old system stayed live throughout. There was no maintenance window and no cutover weekend.
- A native mobile app was already in the App Store with installed users. It had to keep working, on the same binary, while the backend underneath it changed completely.
- Salesforce had to stay in sync in both directions, without creating duplicate contacts in an org with an active duplicate-detection rule.
03 What we built
- A single Laravel + Filament platform on one Postgres database, with per-campus tenancy enforced by row-level security.
- A per-campus admin panel, plus a cross-campus HQ panel for the parent organisation. The same data, two scopes, one permission model.
- A drag-and-drop public site builder so each campus runs its own public-facing site out of the same platform.
- Two-way Salesforce sync built on bridge objects, with flow deployment via the Metadata API.
- A read-only AI admin assistant, walled off from writes at the database layer.
- The v2 API the existing mobile app now runs against.
Row-level security that silently did nothing
Tenant isolation was designed on Postgres row-level security: every tenant table gets a policy, the request sets the current organisation, the database refuses to return anyone else's rows. Policies were in place. The tests passed. And one campus could still have read another's students.
The managed Postgres had provisioned the application's database user as the cluster's bootstrap superuser. Superusers bypass row-level security unconditionally. FORCE ROW LEVEL SECURITY does not apply to them. And the bootstrap user cannot be demoted; Postgres refuses. So the entire isolation layer was inert, and nothing about the running application looked wrong.
The fix was a separate non-superuser role, with ownership of every table and sequence in the schema reassigned to it, and the application moved onto it. current_user is now asserted on boot, because a future deploy that quietly reverts the credential would turn tenant isolation back off without a single error.
Then the same property bit from the other direction. A migration backfilling a new column looped over an existing table and wrote nothing. It ran under row-level security with no bypass active, so the read returned zero rows and the loop did nothing, successfully. 152 student records went blank and the migration reported success. Anything crossing tenants now has to say so explicitly.