Add TypeScript + Express + Postgres backend with Better Auth

Implements the first temetro backend: an Express 5 API on Postgres via
Drizzle ORM, with authentication and multi-tenant clinics powered by
Better Auth.

- Auth: email/password with required email verification, password reset,
  rate limiting, CSRF/trusted-origins, secure cookies, session audit hook.
- Organizations (clinics) with RBAC (owner/admin/member/viewer) and an
  extended `patient` permission set; member invitations by email.
- Org-scoped patient records mirroring the frontend Patient shape, with
  CRUD endpoints gated by permission (read/write/delete).
- Email helper logs links to the console when SMTP is unset (zero-setup
  local dev); Dockerfile + docker-compose (db + backend + frontend) with
  migrations applied on startup and a configurable Postgres host port.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-02 21:27:32 +03:00
parent a39ecbe600
commit 9dabe2f5d2
31 changed files with 8576 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
// Request properties populated by our auth middleware.
declare global {
namespace Express {
interface Request {
user?: {
id: string;
email: string;
name: string;
emailVerified: boolean;
};
session?: {
id: string;
userId: string;
activeOrganizationId?: string | null;
};
organizationId?: string;
memberRole?: string;
}
}
}
export {};
+72
View File
@@ -0,0 +1,72 @@
// Canonical patient shape — mirrors frontend/lib/patients.ts so API responses
// can be consumed by the chat record cards without any reshaping on the client.
export type AllergySeverity = "mild" | "moderate" | "severe";
export type LabFlag = "normal" | "high" | "low" | "critical";
export type Sex = "M" | "F";
export type PatientStatus = "active" | "inpatient" | "discharged";
export type Allergy = {
substance: string;
reaction: string;
severity: AllergySeverity;
};
export type Medication = {
name: string;
dose: string;
frequency: string;
};
export type Problem = {
label: string;
since: string;
};
export type Vitals = {
bp: string;
hr: string;
temp: string;
spo2: string;
takenAt: string;
};
export type Lab = {
name: string;
value: string;
flag: LabFlag;
takenAt: string;
};
export type Encounter = {
date: string;
type: string;
provider: string;
summary: string;
};
// A short series for a sparkline; `points` are most-recent-last.
export type Trend = {
label: string;
unit: string;
points: number[];
};
export type Patient = {
fileNumber: string; // MRN / file number, e.g. "10293"
name: string;
age: number;
sex: Sex;
pcp: string; // primary care provider
status: PatientStatus;
initials: string; // for AvatarFallback
allergies: Allergy[];
alerts: string[];
medications: Medication[];
problems: Problem[];
vitals: Vitals;
vitalsTrend: Trend; // headline vital plotted as a sparkline
labs: Lab[];
labTrend: Trend; // headline lab plotted as a sparkline
encounters: Encounter[];
};