Health SaaS Platform
One clinic-management platform that reshapes itself for every clinic type.
- Status
- In Development
- Updated
- July 8, 2026
Problem
Clinics are not interchangeable. A dental practice charts teeth; a general practice doesn't. Most clinic software pretends otherwise and ships one rigid product to everyone. The goal here was the opposite: one platform, infinite clinic types — a system that adapts its feature set to each organization, while guaranteeing that no clinic can ever see another's patient data.
Two hard requirements fell out of that: strict multi-tenant isolation, and per-tenant configurability that doesn't mean forking the codebase.
Architecture
A TanStack Start SPA (React 19, shadcn/ui, Zustand) talks to a modular NestJS API. Tenancy is shared-database and row-level: every core entity carries an organizationId, and isolation is enforced structurally by a chain of guards that runs before any controller.
Every request passes through three guards, in order. Each has one job, and the order matters — you authenticate before you resolve a tenant, and you resolve a tenant before you check what that tenant is allowed to do.
- JwtAuthGuard validates the bearer token (public routes opt out).
- TenantContextGuard reads the
x-organization-id/x-location-idheaders, verifies the user actually belongs to that organization, and attaches a verifiedtenantContextthat every downstream service reads instead of trusting the client. - FeatureFlagGuard enforces
@RequireFeature('key')— a route for a capability the tenant hasn't enabled simply doesn't exist for them.
The domain is broad: patients (with 15+ sub-entities — charts, vitals, notes, treatments, prescriptions, documents, consents), appointments with conflict detection, employees, payroll, and billing. Documents and generated PDFs go to Cloudinary; real-time updates flow over Socket.io.
Key decisions & trade-offs
Shared database, row-level tenancy — not a database per tenant. One schema, one migration path, one connection pool, isolation enforced in the application layer. Database-per-tenant would give harder isolation guarantees, but the operational cost (migrations × tenants, connection sprawl) isn't worth it at this scale. The mitigation is that isolation is structural — the guard chain resolves and verifies the tenant once, so a forgotten where clause can't leak across tenants because services never see an unverified org id.
Feature flags as product surface, not config. Each organization gets a seeded catalog of capabilities, toggled per org and cached in memory as a Map<orgId, Set<featureKey>> so the check is a hash lookup, not a query. This is what makes "infinite clinic types" real: a dental clinic and a general practice run the same binary and see different products. The cache is invalidated on any flag change, so toggling takes effect without a redeploy.
Composite indexes led by organizationId. Because nearly every query is "these rows, for this org," indexes like (organizationId, status) keep tenant-scoped reads fast even as shared tables grow across all tenants.
synchronize in dev, migrations in prod. Fast iteration while the schema churns, with generated TypeORM migrations as the production path.
Outcome
The platform is in active development. The tenancy model and feature-flag engine are working end to end — auth, organizations, locations, employees, appointments, patients, and payroll are implemented, with the flag system driving per-clinic behaviour.