ClinicBot · Build Crew · Contributing
Five people are going to own this end-to-end for years. These rules aren't ceremony — they're what lets all five of us move fast in the same codebase without breaking each other, and what makes a new vendor a one-file change instead of a rewrite.
Each module under src/modules is a bounded context with one job, mirroring the five vectors. If a file keeps growing, it's doing too much — split it.
A new clinic = a DB row. A new integration = a new adapter implementing an existing port. The core loop never gets edited to add either.
The Drizzle repo and the in-memory repo are interchangeable. Tests swap fakes for real adapters and the use case can't tell — that's the proof the contract holds.
MessagingChannel only sends. ClinicResolver only resolves. No fat interface forces an adapter to implement methods it doesn't need.
Domain code depends on ports.ts, never on a vendor SDK. Concrete adapters are wired only in src/server/composition.ts — the composition root.
Never import a vendor SDK into domain code. The domain (bot-brain, booking) talks to interfaces. Vendors are adapters. This is the whole reason a clinic can't lock us into Practo and a competitor can't copy us cheaply.
✗ Don't
✓ Do
Say V4 wants to add a payment provider. Four small steps, zero domain edits:
1. Define / reuse a port: src/modules/integrations/payments/ports.ts
export interface PaymentProvider { ... }
2. Write the adapter: adapters/razorpay.payment.ts
export class RazorpayPayment implements PaymentProvider
3. Write a fake for tests: adapters/in-memory-payment.ts
4. Wire it in the root: src/server/composition.ts
hasRazorpay ? new RazorpayPayment() : new InMemoryPayment()
That's it. Nothing in bot-brain or booking changes.
clinic_id. Every query is scoped to it.runWithClinic(clinicId, …) so Row-Level Security applies.clinic_id from the client — it comes from the tenant router (the WhatsApp number) or the authenticated session.handle-inbound-message.test.ts) is the template: wire fakes, run the use case, assert.pnpm typecheck # no type errors
pnpm test # all green, new behaviour covered
pnpm lint # clean
pnpm format # formatted
runWithClinic.