Full-stack TypeScript monorepo for managing a Swiss TPI (Travail Pratique Individuel) project "AD Account Manager". Features Gantt timeline and Kanban board views. Available as a hosted web app (PostgreSQL + better-auth) and as an offline desktop app (Tauri + SQLite).
- Runtime: Bun
- Frontend: React 19 + Vite + Tailwind CSS 4
- Backend: Express 5 (running on Bun)
- Database (web): PostgreSQL 18 (Docker) + Drizzle ORM
- Database (desktop): SQLite via bun:sqlite + Drizzle ORM
- Desktop shell: Tauri v2 (Rust + WebView2/WebKitGTK)
- API Contract: OpenAPI 3.1 + Orval codegen
apps/web - React frontend (Vite)
apps/api - Express REST API (Bun)
apps/desktop - Tauri v2 shell (Rust, src-tauri/)
packages/db - Drizzle ORM schema & connection (PostgreSQL)
packages/desktop-db - Drizzle ORM schema (SQLite mirror)
packages/desktop-api - Bun sidecar wrapper for desktop mode
packages/api-spec - OpenAPI spec + Orval config
packages/api-client-react - Generated React Query hooks (DO NOT EDIT generated/)
packages/api-zod - Generated Zod schemas (DO NOT EDIT generated/)
docker compose up -d # Start PostgreSQL
bun install # Install dependencies
bun run db:push # Push schema to DB
bun run db:seed # Seed initial data
bun run dev:api # Start API on :3001
bun run dev:web # Start frontend on :5173bun install # Install dependencies (includes @tauri-apps/cli)
bun run build:desktop # Build frontend + sidecar (auto-detects OS)
bun run tauri:dev # Launch the Tauri desktop app in dev modeProduction build (produces .deb/.rpm on Linux, NSIS/.msi on Windows):
bun run tauri:buildbun run dev- Start all web servicesbun run dev:web- Frontend onlybun run dev:api- API onlybun run build:desktop- Build frontend + sidecar (platform auto-detect)bun run tauri:dev- Launch desktop app in dev modebun run tauri:build- Build desktop installers for the current platformbun run tauri:build:appimage- Build AppImage with sidecar workaround (Linux only)bun run db:push- Apply Drizzle schemabun run db:seed- Seed databasebun run codegen- Regenerate API client from OpenAPI specbun run typecheck- TypeScript check (all 7 packages)bun run lint- Lint code
DATABASE_URL- PostgreSQL connection string (default: postgresql://tpi:tpi@localhost:5432/tpi_flow)PORT- API server port (default: 3001)DISABLE_ENCRYPTION+IS_DESKTOP- Both required to bypass field encryption in desktop modeCSP_ENFORCE/CSP_REPORT_URI- Toggle CSP enforce mode and set the reporting endpoint.POSTMARK_API_TOKEN- Postmark Server API token for transactional email (reset-password, email verification). When unset,sendEmail()no-ops with a console warning so local dev keeps working without Postmark.EMAIL_FROM- Sender address for all transactional emails (default:TPI-Flow <noreply@tpiflow.ch>). Domain must be verified in Postmark (DKIM + Return-Path).EMAIL_REPLY_TO/POSTMARK_MESSAGE_STREAM- Optional overrides for Reply-To header and Postmark stream (default stream:outbound).INACTIVITY_WARN_DAYS/INACTIVITY_DELETE_DAYS- Inactive-account cleanup thresholds (defaults: 7 / 30 days). Final reminder fires atDELETE_DAYS - 1. Override only for tests; admins (role='admin') andADMIN_PROTECTED_EMAILSare always exempt.INACTIVITY_CRON- Cron expression for the inactivity job (default:0 3 * * *Europe/Zurich).
- Never edit files in
*/generated/directories - Update
packages/api-spec/openapi.yamlfirst, then run codegen - Use
bunfor everything (install, run, etc.) - Routes read
dbandtablesfromreq.app.locals(not module-level imports) packages/desktop-dbschema must stay in sync withpackages/dbschemapackages/desktop-api/src/init-schema.tsDDL must stay in sync withpackages/desktop-db/src/schema/- When changing the desktop schema: update
init-schema.ts(for fresh installs) AND add a migration inpackages/desktop-api/src/migrator.ts(for existing users)
- AppImage build: Uses a dummy-swap workaround (
scripts/build-appimage.sh) because Tauri's linuxdeploy corrupts Bun sidecar binaries. The script re-signs the repackaged AppImage for the auto-updater. - Auto-updater:
tauri-plugin-updater(Rust) +@tauri-apps/plugin-updater(JS). Frontend hookuseUpdater()checks on startup + hourly.UpdateBannercomponent in the sidebar. No-ops in web mode. - SQLite migrations: Embedded in TypeScript (
migrator.tsMIGRATIONS array) — no filesystem SQL files (incompatible withbun build --compile). Boot sequence:initSchema()→runMigrations()→seedLocalUser(). - Desktop auth stub:
/api/auth/get-sessionreturns a local session JSON in desktop mode souseSession()works without better-auth.
- WebSocket notifications (
apps/api/src/services/socket-broadcaster.ts): Socket.io server attached to the HTTP server fans outMAINTENANCE,CLEAR_MAINTENANCE,NEW_FEATURE,FORCE_LOGOUT,DIRECT_MESSAGEevents via WebSocket. Redis adapter (@socket.io/redis-adapter) propagates events across multiple instances (Railway).MAINTENANCEstate is persisted in the singletonmaintenance_statetable and rehydrated at boot viainitBroadcaster(). - Admin panel (
/admin): RBAC-gated by the session user'srolecolumn (useroradmin).requireAuth+requireAdminmiddleware chain on/api/admin/*, rate-limited viaadminLimiter. Bootstrap the first admin viabun run db:promote-admin <email>(seepackages/db/src/promote-admin.ts). The seed user (test@tpi-flow.local) is admin by default. Exposes broadcast + feedback triage + user management + weekly digest preview endpoints. - In-app feedback:
POST /api/feedback(rate-limited 5/h per IP) writes to thefeedbacktable; onlytitleanddescriptionare encrypted at rest (seeFEEDBACK_FIELDSinpackages/db/src/field-encryption.ts). Admin triage viaGET /api/admin/feedbackandPATCH /api/admin/feedback/:id. - CSP:
apps/api/src/middleware/security.tshashes two inline scripts fromapps/web/index.html(theme bootstrap + JSON-LD). Update both hashes if either script changes — seedocs/DEVELOPMENT.md.connect-srcderives WebSocket origins fromCORS_ORIGINso only same-hostwss://upgrades pass CSP2/CSP3 enforcement. - Transactional email (
apps/api/src/services/email.ts): Postmark wrapper used by better-auth for password reset (sendResetPassword) and email verification (sendVerificationEmail). Templates live inapps/api/src/emails/as React Email components (base-layout, reset-password, verify-email) rendered with@react-email/render. Failures are swallowed and logged so a Postmark outage never blocks the auth flow. User name is decrypted (seeencryptUserName) before being passed to templates. New signups receive a verification email (emailVerification.sendOnSignUp: true) but login is not blocked on unverified email. DNS setup: on Cloudflare, thepm-bouncesCNAME must be "DNS only" (not proxied) or Postmark bounces break. - Inactive account cleanup (
apps/api/src/scheduler/inactivity-cron.ts+services/inactivity.ts): Daily Redis-locked cron (same advisory-lock pattern as the weekly digest) sends a J+7 warning, a J+29 final reminder, and at J+30 deletes the user via the same cascade asDELETE /api/admin/users/:id. Activity is tracked byapps/api/src/middleware/track-activity.ts(1h throttle, fire-and-forget UPDATE) mounted afterrequireAuthon/api. Reconnection clears the warning flags atomically in the same UPDATE. Admins andADMIN_PROTECTED_EMAILSare exempt. Admin bypass:POST /api/admin/users/:id/trigger-inactivity-warningskips the 7d wait by backdatinglastActivityAt. CGU section 11.3 documents the policy.