
The whole platform for a compliance firm, from the public site to the portal clients log into, all running on one API I designed and built.
A firm outgrowing its tools
Fractional Coordinator runs Title IX, Title VI and ADA investigations for colleges, school districts and employers, and trains the people who do that work.
When I started, there was a public site in its own repo, a client portal that was a static shell of mock data, and signed contracts copied out of Google Drive by hand.
One platform the firm can run on
They needed a public site, a client portal, a console to run the firm and paid training. I built it from scratch, against three goals.
- Goal 1Each institution sees only its own work, and access ends the day an engagement does.
- Goal 2Staff publish, sell training and file contracts without a developer.
- Goal 3Every rule is written once and enforced the same way in every app.
One codebase, one source of truth
Four audiences meant four front ends, with rules that overlap almost entirely. The first site had already shown what happens when they live apart: its form fields drifted from the API, and submissions were dropped in silence.
So everything moved into one Turborepo, sharing Zod contracts that turn that drift into a compile error. The rules live in one NestJS API, because half its callers, like Stripe and a Drive script, are not Next.js at all.
Trade‑offMore build plumbing, and a second deploy target to keep in step with the repo.

Access that fails closed
In a compliance portal, a leak is the worst possible bug. Grants hang off an institution membership, not the person, so removing a membership removes every grant with it.
Every API route must declare the grant it needs, and a route that declares nothing is refused, so forgetting a check locks a route instead of opening it. Sessions live in Postgres, so access ends the moment an engagement does, and there is no public sign‑up.
Trade‑offA membership lookup on every request, and a decorator on every route.
See the codeapps/api/src/database/schema/tenancy.schema.ts · portal/portal-grant.guard.ts
A grant row cascades with the membership it hangs off. The guard refuses a route that declares no grant at all, so forgetting the decorator locks a route instead of opening it.
organizationMemberId: text('organization_member_id')
.notNull()
.references(() => organizationMember.id, { onDelete: 'cascade' }),
grantKey: text('grant_key').notNull(),
// PortalGrantGuard.canActivate
if (required === undefined) {
throw new ForbiddenException({
code: 'GRANT_NOT_DECLARED',
message: 'This route declares no entitlement',
});
}
// ...
if (!membership.grants.includes(required)) {
throw new ForbiddenException({ code: 'GRANT_REQUIRED', grant: required });
}


Files that never pass through the API
Case files are confidential, and some are large. They live in a private R2 bucket, reached only through links that expire after five minutes, so bytes never cross the API and a forwarded link soon stops working.
Signed contracts now file themselves. A script watches each institution’s Drive folder and sends new PDFs to the API. Each institution is bound to one folder, so even a leaked script secret cannot file into the wrong portal.
Trade‑offUp to ten minutes before a signed contract appears in the portal.
See the codeapps/api/src/signed-contracts/signed-contracts.service.ts
Every step of the sync re-checks the folder binding, and the file is read back before it is filed: right size, and it starts like a PDF.
private async assertLinked(organizationId: string, folderId: string): Promise<void> {
const [org] = await this.db
.select({ folderId: organization.driveContractsFolderId })
.from(organization)
.where(eq(organization.id, organizationId));
if (!org || org.folderId === null || org.folderId !== folderId) {
throw new NotFoundException();
}
}
// register()
if (bytes.length !== input.byteSize) {
return this.fail(input, 'The uploaded file is not the declared size.');
}
if (bytes.subarray(0, PDF_MAGIC.length).toString('latin1') !== PDF_MAGIC) {
return this.fail(input, 'The uploaded file is not a PDF.');
}
Training that unlocks itself
Courses, Knowledge Hub memberships and Playbook licenses sell through Stripe Checkout. One webhook settles every purchase: it matches the buyer’s email to a portal member and unlocks access for their institution. There is no cart and no card data on our side.
Trade‑offA buyer without a portal account cannot be matched automatically, so those purchases land in a console list for staff.

What it became
Five apps on their own subdomains, all calling one API that checks the session, the role and the institution before anything else happens.
cookie
only
Numbers mark the chapters behind each part. Point at one to see what it touches.
Where it landed
The firm now runs its public site, client work, contracts and training on one platform. Back to the three goals:
- ✓ Goal 1Each institution sees only its own work.Every route checks its grant, and ending an engagement ends access at once.
- ✓ Goal 2Staff run it without a developer.Editors publish without a deploy, contracts file themselves and payments unlock access on their own.
- ✓ Goal 3Every rule is written once.Five apps share one API and one set of contracts.
Trade‑offs I live with
- A separate API deploy that has drifted from the repo more than once.
- Up to ten minutes before a signed contract shows up.
- Buyers without an account still need a person to match them.
I shipped it in thirteen vertical slices, one pull request each, so every step left working software behind.
