Code Organization

Code Organization — File Modularity

Multiple developers and AI agents work on separate issues in parallel. To minimise merge conflicts and keep pull requests small and reviewable, the architecture enforces a strict "one concern per file" rule across every layer.


General Principles

One component / function / model per file

Each React component, FastAPI router, Pydantic model, durable job, and utility function lives in its own file. Prefer many small files over a few large ones.

Barrel files for public API only

index.ts (TypeScript) and __init__.py (Python) files re-export the module's public surface. Implementation stays in dedicated files beside the index. This means two developers can add new items to the same module without touching the same lines.

Co-locate tests appropriately

  • TypeScript/React: Test files live next to source (Button.tsxButton.test.tsx).
  • Python: Tests go in a tests/ directory with filenames matching source modules.

Flat over nested

Two directory levels deep is fine. Five hinders discoverability. Avoid unnecessary nesting.


Frontend — ui/web-app & ui/components

ui/web-app is the applications UI; ui/components is a shared component/library workspace (one of the ui/* shared packages — see Monorepo) that houses reusable presentational components consumed by ui/web-app. The same one-concern-per-file rule applies to both.

✅ Prefer❌ Avoid
components/Button.tsx — one component per filecomponents/index.tsx containing Button, Card, Modal
components/Button.test.tsx — co-located test__tests__/components.test.tsx testing many components
hooks/useAuth.ts — one hook per filehooks/index.ts with multiple hook definitions

Re-export from barrel files:

// components/index.ts
export { Button } from "./Button";
export { Card } from "./Card";

Backend — backend/api (HTTP) & backend/worker (durable)

backend/api is the FastAPI HTTP service; backend/worker holds the Restate durable-execution handlers. The same one-concern-per-file rule applies to both.

✅ Prefer❌ Avoid
routers/health.py, routers/patients.py (backend/api)routers.py with all route handlers
models/patient.py, models/appointment.pymodels.py with every Pydantic model
jobs/sync.py, jobs/notify.py (backend/worker)jobs.py with all durable handlers

Schema — schema/ (Drizzle)

Tables, RLS, and migrations live in the top-level schema/ package — the single source of truth that Drizzle Kit owns and the Python api reflects (no Alembic). One concern per file:

schema/
├── src/
│   ├── tables/          # one Drizzle table (concern) per file
│   │   ├── patient.ts
│   │   └── appointment.ts
│   └── rls.ts           # crudPolicy / authenticatedRole RLS definitions
├── drizzle.config.ts    # Drizzle Kit config
└── migrations/          # generated SQL — one migration history
✅ Prefer❌ Avoid
tables/patient.ts, tables/appointment.ts — one table per filetables.ts / schema.ts with every table
Generated SQL committed under migrations/Hand-edited or duplicated migration histories

Authorization policies — policies/ (Cerbos)

Cerbos policy-as-code lives in a version-controlled, top-level policies/ directory (Cerbos YAML), loaded by the self-hosted Cerbos PDP (see Backend). One resource per file:

✅ Prefer❌ Avoid
policies/patient.yaml, policies/appointment.yaml — one resource policy per filepolicies.yaml with every resource's rules

Infrastructure & Config

  • One Pulumi resource group per file in infra/ (e.g., gcp.ts, neon.ts, cloudflare.ts).
  • One dbt model per .sql file (dbt enforces this).
  • One GitHub Actions workflow per .yml file.

Why This Matters for AI-Assisted Development

When AI coding agents (Copilot, Copilot Workspace, or third-party agents) work on issues concurrently, each agent typically modifies a small set of files. If unrelated features share a file, the agents' branches produce conflicting diffs on the same file, requiring manual resolution.

Granular files mean:

  1. Zero conflicts. Two agents adding two different components create two different files.
  2. Smaller PRs. Each change touches only the files it needs to.
  3. Easier reviews. Human and automated reviewers see focused, relevant diffs.
  4. Trivial reverts. Removing a feature is as simple as deleting its file(s).

When in doubt, split it out.