Multi-tenant veterinary-hospital operations platform. The current product scope is physical-asset operations: equipment tracking & custody, waitlist and operational-state lifecycle, Code Blue (emergency) workflows, crash-cart checks, inventory / dispense, shifts & scheduling, and external PMS integrations. It ships as an offline-first PWA and a Capacitor native iOS/Android shell that runs the same web bundle.
Scope note: ER / patient / hospitalization / medication-formulary / pharmacy-forecast features were removed in migrations 142–143 (
docs/scope-change-2026.md). Thevt_appointmentstable is the unified task model, rendered in the UI as "Tasks / משימות". Legacy routes (/patients,/er,/billing,/meds) survive only as redirects to equipment surfaces.
For the full reverse-engineered design see ARCHITECTURE.md; for
the operating doctrine and frozen-surface contracts see CLAUDE.md.
| Frontend | React 18 · Vite 7 · TypeScript · wouter routing · TanStack Query · Zustand · Tailwind/shadcn · RTL (Hebrew default) · PWA/offline-first (Dexie + service worker) |
| Backend | Express 4 · TypeScript · Drizzle ORM 0.45 · PostgreSQL (pg) · Server-Sent Events realtime (+ an additive Socket.io collaboration channel) |
| Jobs | BullMQ 5 + Redis (ioredis) — workers & schedulers; Redis optional in dev, required in prod |
| Auth | Clerk (required in production) or dev-bypass (non-production only — no Clerk secret, or explicit CLERK_ENABLED=false) |
| Native | Capacitor 8 (ios/, android/) wrapping the built web bundle |
| Observability | Sentry (@sentry/node, @sentry/react) |
| Deploy | Railway (railway.json, Dockerfile, nixpacks.toml) |
Runtime: Node ≥ 22.12 and < 25 (Node 25+ is refused by engine-strict), pnpm 9.15.9.
Clients (PWA · Capacitor native · display kiosks)
│ REST + SSE
▼
Express API (server/index.ts → server/app/routes.ts, 60 router mounts)
middleware: helmet/CSP → cors → compression → clerk → rate-limit
→ i18n → tenantContext → sessionContext
├─ routes/ one file per API resource
├─ services/ domain logic (equipment, dispense, waitlist, tasks…)
├─ domain/ hexagonal evidence-graph + Asset Copilot
├─ lib/ authority+enforcement, realtime outbox, audit, metrics…
├─ integrations/ external PMS adapters (inbound/outbound, sync, conflicts)
└─ workers/+jobs/+queues/ BullMQ workers & schedulers
│ │ │
▼ ▼ ▼
PostgreSQL Redis/BullMQ External PMS
Drizzle, vt_* (jobs, push, (priza, vendor-x,
tables integrations) generic adapters)
Request lifecycle (mutation): typed client call (src/lib/api.ts) → emergency-endpoint
classifier → Express middleware chain → router → Zod validation → authority/enforcement →
domain service → clinicId-scoped Drizzle write → vt_event_outbox row (+ fire-and-forget
audit) → outbox publisher → SSE broadcast → client cache invalidation.
Hard rules: every query filters by clinicId; role is read from vt_users.role, never
JWT; the realtime (SSE+outbox), PWA build-tag cache, emergency-endpoint cache
denylist, and authority off|shadow|enforce envelope are frozen contracts.
| Domain | What it does | Entry points |
|---|---|---|
| Equipment | Asset CRUD, custody/checkout/return, waitlist + reservation, operational-state lifecycle, RFID/scan location inference, ward/equipment display board, Asset Copilot (evidence-graph explanations) | routes/equipment*.ts, |
routes/rooms.ts, routes/returns.ts, routes/display.ts, services/equipment-*.ts, domain/equipment/** |
||
| Code Blue / safety | Emergency sessions (online-only, server-confirmed), presence, crash-cart checks, reconciliation scanner | routes/code-blue.ts, routes/crash-cart.ts, lib/code-blue-*.ts |
| Authority & enforcement | Effective clinical authority + per-clinic evaluator families (off | shadow | enforce) with Strategy-A safety net |
lib/authority.ts, lib/authority/enforcement/*, middleware/authority.ts |
| Inventory / dispense | Containers, items, dispense (idempotent), restock, procurement/POs, shadow-inventory reconciliation | routes/{containers,dispense,restock,inventory-items,procurement}.ts, services/*.ts |
| Tasks & shifts | Unified task model, task intelligence/automation/recall, shifts, clinical check-in, shift chat, alerts | routes/{tasks,appointments,shifts,clinical-check-in,shift-chat,alert-acks}.ts |
| Integrations | External PMS adapters, HMAC inbound webhooks, canonical mapping, conflict engine, rollout/resilience, health dashboard | server/integrations/**, routes/integrations.ts |
| Realtime | SSE stream, outbox publisher, replay, keepalive, DLQ/janitor health; plus an additive, isolated Socket.io collaboration channel (/collab-ws) for presence/typing/cursors — ephemeral only, never domain state |
|
routes/realtime.ts, lib/event-publisher.ts, lib/outbox-*.ts, lib/realtime-collab/*, src/lib/collab-socket.ts |
||
| Platform/infra | Auth mode, tenant context, i18n, rate limiting, audit, metrics, config crypto, push, uploads | server/middleware/*, server/lib/*, routes/{users,push,uploads,support}.ts |
| Frontend shell | Provider stack, lazy route table, platform split (native vs web), offline sync, SW lifecycle | src/main.tsx, src/app/routes.tsx, src/{native,desktop,shell}/*, src/lib/* |
Server startup (server/index.ts):
env-bootstrap (loads .env.local then .env; OS env always wins) → Sentry init → validateEnv() → build
Express app → health routes (bypass middleware) → security/CORS/compression → raw-body
webhook mounts (Clerk svix, integration HMAC, RFID) → express.json() + global XSS
sanitizer → conditional Clerk middleware → /api rate-limit → i18n → tenant → session →
registerApiRoutes() → (prod) SPA static serving → app.listen(PORT) →
runMigrations() → ensureClinicPhase2Defaults() → startBackgroundSchedulers(). After
app.listen, the additive Socket.io collaboration channel is initialized on the shared HTTP
server via a non-fatal dynamic import (R-RTC-1) — any failure logs and leaves the channel
disabled while SSE, vt_event_outbox, and Code Blue start normally.
Frontend startup (src/main.tsx): React root → provider stack (QueryClient, Settings,
Confirm, Clerk auth, error boundary, Sync) → register build-tag-versioned service worker (web)
or construct native Clerk instance (Capacitor) → App → wouter route table (src/app/routes.tsx).
Background work (server/app/start-schedulers.ts, skipped in tests): outbox publisher,
outbox janitor + DLQ scanner, job runtime (expiry/charge-alert/stale-checkin), integration
worker + crons, task-ownership backfill/sweep, shadow inventory, Code Blue reconciliation,
five equipment operational-state workers, emergency-dispense scanner, and push/health/watchdog
schedulers.
# Prerequisites: Node >= 22.12 and < 25 (nvm use), pnpm 9.15.9, PostgreSQL, Redis (optional in dev)
pnpm install
cp .env.example .env # fill DATABASE_URL etc.
pnpm db:migrate # apply all migrations (also runs at server startup)
pnpm dev # API :3001 + Vite :5000 (predev frees the ports)Minimal dev .env (omit Clerk keys to use dev-bypass auth — hardcoded admin, no Clerk SDK):
DATABASE_URL=postgres://vettrack:vettrack@localhost:5432/vettrack
SESSION_SECRET=dev-session-secret-for-local-development
NODE_ENV=development- Env precedence (highest → lowest): OS env (
process.env) →.env.local→.env.dotenvnever overwrites an already-set variable, so OS/Railway/CI values win (server/lib/env-bootstrap.ts). Full reference:.env.example. - Auth mode (resolved at startup by
server/lib/auth-mode.ts):- dev-bypass — non-production
NODE_ENVonly, when noCLERK_SECRET_KEYis set (orCLERK_ENABLED=falseis the explicit override) → hardcodedDEV_USER(admin,clinicId = dev-clinic-default). In production this path is gated off (server/middleware/auth.ts) andvalidateEnv()requires Clerk, so omitting keys does not silently enable the bypass. - clerk —
CLERK_SECRET_KEYpresent (andCLERK_ENABLED !== "false") → full Clerk JWT validation
- dev-bypass — non-production
- Role hierarchy (numeric):
admin=40 · vet=30 · senior_technician=25 · lead_technician=22 · vet_tech=20 · technician=20 · student=10. Always sourced fromvt_users.role. - Required production env vars (enforced at startup by
server/lib/envValidation.ts):DATABASE_URL(orPOSTGRES_URL),REDIS_URL,SESSION_SECRET,CLERK_SECRET_KEY,VITE_CLERK_PUBLISHABLE_KEY,ALLOWED_ORIGIN,CLERK_WEBHOOK_SECRET,DB_CONFIG_ENCRYPTION_KEY(AES-256-GCM for integration creds invt_server_config),DATA_INTEGRITY_HEALTH_TOKEN,DB_SSL_REJECT_UNAUTHORIZED,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY. - Feature/rollout flags via
server/lib/feature-flags.tsandintegrations/feature-flags.ts. - Collaboration channel flags (
server/lib/realtime-collab/config.ts, all optional — the channel is safe-by-default):COLLAB_WS_ENABLED(default on; setfalseto kill the channel),COLLAB_WS_ALLOW_SINGLE_INSTANCE(permit the bounded in-process presence fallback when Redis is absent — production otherwise requires the Redis adapter for cross-instance fan-out),COLLAB_WS_ALLOWED_ORIGINS/APP_ORIGINS(handshake Origin allowlist for the CSWSH defense).
- Schema change → edit
server/schema/*.ts→ hand-writemigrations/NNN_description.sql(idempotent statements; checkmigrations/for the current tail) → commit it →pnpm db:migrate.drizzle-kitis not the authoring path and is not installed — seedocs/migrations.md. - New API route → add
server/routes/<x>.ts→ register inserver/app/routes.ts. - New worker/scheduler → register in
server/app/start-schedulers.ts. - API surface → typed function in
src/lib/api.ts+ type insrc/types/. - New page →
src/pages/+ lazy<Route>insrc/app/routes.tsx. - User copy → keys in
locales/he.json+locales/en.json(parity enforced); access via typedt. - New audit kind → extend the closed
AuditActionTypeunion inserver/lib/audit.ts. - New telemetry → bounded enum on client +
server/routes/realtime.ts+ closed union inserver/lib/metrics.ts. - Always run
pnpm typecheck(zero errors). Realtime/Code Blue/PWA changes need browser verification (Playwright drills).
Architecture gates: pnpm architecture:gates (dependency-cruiser, madge cycles, tenant-query
lint, route contract, query-key audit), pnpm knip (dead code), pnpm i18n:check (locale parity).
| Command | Description |
|---|---|
pnpm dev |
API (:3001) + Vite (:5000) concurrently |
pnpm build |
Production frontend build → dist/public |
pnpm start |
Production server (NODE_ENV=production) |
pnpm worker |
Standalone notification worker CLI (Redis required) |
pnpm typecheck |
tsc --noEmit for app + server configs |
pnpm test |
Vitest unit/integration suite |
pnpm db:migrate |
Apply pending migrations |
pnpm validate:prod |
Pre-deployment checks |
pnpm auth:preflight |
Verify Clerk config + auth mode |
pnpm docs:audit |
Regenerate docs/audit/* route & schema inventories |
pnpm cap:build:native / :android |
Build bundled Capacitor shell |
pnpm cap:install:ios-sim |
Build + install on iOS Simulator (macOS + Xcode only) |
- Vitest (
pnpm test) — unit + integration. Several groups are excluded by default invite.config.ts: DB-integration tests (needDATABASE_URL+ migrations), live-server tests (need a running:3001), and the Phase 9 Playwright browser drills. - DB / ops integration:
pnpm test:db-integration,pnpm test:integration:ops. - Playwright E2E:
pnpm test:playwright:*(waitlist, pwa, phase9, signup, workday, ui-smoke). Chromium is pre-provisioned in the cloud environment — do not runplaywright install. - Phase 9 realtime/PWA: deterministic counter contracts in
tests/phase-9-deterministic-drills.test.ts- browser harness
tests/phase-9-drills.spec.ts.
- browser harness
Coverage target is 80%+ (see .claude/rules/ecc/common/testing.md); prefer the AAA pattern.
- Clerk — auth + user webhooks (
/api/webhooks/clerk, svix-verified raw body). - PMS adapters —
priza,vendor-x,generic-pms,local-sandboxover a commonbase(server/integrations/adapters/*). Inbound webhooks are HMAC-verified over raw body with a CIDR allowlist (/api/integration-webhooks/:adapterId); outbound sync + conflict resolution via canonical mappers and a conflict engine. Circuit breaker, rate limits, and staged rollout policy live inserver/integrations/resilience/*, with rollout gating inserver/integrations/feature-flags.tsandserver/integrations/vendor-x-rollout.ts(was deleted inserver/integrations/rollout/*6504be25a). - Redis / BullMQ — job runtime, push fan-out, integration sync.
- AWS S3 — uploads/storage (
@aws-sdk/client-s3, multer). - Web Push (VAPID) — push notifications.
- Anthropic — Asset Copilot explanations (
server/lib/anthropic-client.ts). - Sentry — error/perf monitoring on client and server.
src/ React frontend (PWA, RTL, offline-first)
app/ wouter route table + platform guards
pages/ route-level page components (all lazy-loaded)
features/ feature modules (auth, equipment, inventory, shift-chat, …)
components/ shared UI (shadcn/Radix primitives in components/ui/)
native/ desktop/ Capacitor native shell vs web shell (selected by PlatformRouter)
shell/ legacy re-export aliases over native/ + desktop/
core/ infrastructure/ emerging hexagonal layer (entities/use-cases/ports + adapters)
hooks/ lib/ auth/push/settings/offline hooks · api client, offline-db, sync-engine, realtime, collab-socket, i18n
server/
index.ts Express entry (env-bootstrap first)
app/ routes.ts (route registration) + start-schedulers.ts
routes/ one file per API resource (60)
schema/ Drizzle pgTable definitions (barrel index.ts, re-exported via db.ts)
services/ domain services
domain/ hexagonal equipment evidence-graph + Asset Copilot
lib/ business logic: authority/enforcement, realtime outbox, realtime-collab (Socket.io), audit, metrics, push…
integrations/ external PMS adapter layer
jobs/ queues/ workers/ BullMQ runtime, queue defs, worker implementations
middleware/ auth, tenant-context, rate-limiters, authority, validate, idempotency
lib/ i18n utilities shared by frontend + backend
locales/ en.json, he.json (Hebrew default; parity enforced)
shared/ constants + types shared across front/back
migrations/ 170+ SQL files (numbered through 171), applied in order at startup
scripts/ dev/ops + architecture-gate scripts
public/sw.js service worker (build-tag cache, emergency endpoint denylist)
ios/ android/ Capacitor native projects
docs/ architecture, runbooks, integrations, audit inventories
- Multi-tenant by
clinicIdon every table and every query. - Outbox-backed SSE realtime — monotonic cursor ordering, HTTP replay, keepalive. The domain/emergency event path is SSE-only (never WebSockets/polling) and stays a frozen contract; emergency endpoints are never cached.
- Additive Socket.io collaboration channel (R-RTC-1) — a separate, isolated transport on
/collab-wscarrying only ephemeral presence/typing/cursor signals. It never carries domain or emergency state, does not replace the SSE path, and its startup is non-fatal (SSE, outbox, and Code Blue run normally if it fails to initialize). - Offline-first sync engine — Dexie cache + FIFO queue + circuit breaker; emergency mutations are intercepted and fail loud (never queued).
- Authority
off | shadow | enforceevaluators per clinic, with a preserved Strategy-A shift-derived fallback and fail-open carve-out. - Closed unions for audit kinds and telemetry counters (extend the union, never free-form).
- Container/presentational + server-state separation on the frontend (TanStack Query for server state, Zustand/URL for client state).
- Platform split via
resolvePlatformTarget()/usePlatformTarget()(mobile | desktop | marketing) →PlatformRouterwraps mobile inNativeShell, desktop/marketing pass through to per-page web chrome (src/app/platform). - Emerging hexagonal core (
src/core+src/infrastructure,server/domain) layered over the older service/lib style — migration is partial. - Architecture enforced in CI — dependency-cruiser, madge cycle checks, tenant-query lint, route-contract extraction, knip dead-code, i18n parity.
ARCHITECTURE.md— full module/dependency/flow referenceCLAUDE.md— operating doctrine & frozen-surface contractsdocs/README.md— documentation indexdocs/scope-change-2026.md— removed domainsdocs/architecture/— ADRs, domain boundaries, offline/realtime invariantsdocs/audit/db.md— generated schema inventorydocs/integrations-guide.md,docs/migrations.md,docs/testing-guide.md,docs/mobile/README.md