ClinicBot · Build Crew · Contributing

How we keep it scalable.

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.

// 01 — SOLID, the ClinicBot way

S — Single Responsibility

One module, one reason to change

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.

O — Open / Closed

Extend by adding, not editing

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.

L — Liskov Substitution

Any adapter is a drop-in

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.

I — Interface Segregation

Small, focused ports

MessagingChannel only sends. ClinicResolver only resolves. No fat interface forces an adapter to implement methods it doesn't need.

D — Dependency Inversion

The domain owns the interfaces

Domain code depends on ports.ts, never on a vendor SDK. Concrete adapters are wired only in src/server/composition.ts — the composition root.

// 02 — The one rule that matters most

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

// in bot-brain
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI(...);
// domain now married to a vendor

✓ Do

// domain depends on a port
function handle(deps: { llm: LlmProvider }) { ... }
// adapter chosen in composition.ts

// 03 — How to add a new integration (recipe)

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.

// 04 — Multi-tenancy is non-negotiable

// 05 — Testing & TDD

// 06 — Definition of done for a PR

pnpm typecheck   # no type errors
pnpm test        # all green, new behaviour covered
pnpm lint        # clean
pnpm format      # formatted