304f7f30c3cd4d25ae9636670a5ab0e4a929183f
Addresses the findings from the platform security audit. Verified green: all-workspace typecheck, web build, 16 API unit tests, 23 e2e auth tests, and 0 high/critical production dependency vulnerabilities. Dependencies (High): - Bump drizzle-orm 0.36→0.45.2 (GHSA-gpj5-g38j-94v9 SQLi-via-identifier) and drizzle-kit→0.31.10; npm audit fix cleared fast-uri path-traversal and the react-router open-redirect. Remaining audit items are dev-only build tooling (esbuild/vite), not shipped at runtime. AI cost control + storage quota (new ai_usage table, migration 0002): - Per-firm monthly AI token budget enforced before each completion (429), with every completion recorded to an ai_usage ledger (lib/ai-usage.ts). - Enforce per-plan storage quota on upload (402) and maintain storage_bytes_used on upload/delete (lib/storage-quota.ts); widen the column int→bigint so 8GB/50GB plans don't overflow. Auth (defense-in-depth): - Constant-time login: verify against a dummy argon2 hash when the account doesn't exist, closing the timing/enumeration oracle (verifyPasswordSafe). - Enforce suspension on requireSuperadmin, /auth/me, /auth/resend-verification. Web: - Validate the post-login ?next= redirect to same-origin paths only (open-redirect / phishing). Deploy hardening: - docker-compose: memory/CPU limits so a spike can't OOM the Dokploy host. - .dockerignore: keep destructive one-off scripts (seed-demo, create-admin, migrate-storage) out of the runtime image; retain the cron scripts. - seed-demo.ts: hard-refuse NODE_ENV=production and the prod DB host. Webhooks / config: - Stripe idempotency via a stripe_events ledger (skip already-processed events; record only after successful processing so a transient failure still retries); make the plan-upgraded email non-blocking. - Rate-limit account export and invoice PDF; cap invoice item arrays at 200. - Require TURNSTILE_SECRET_KEY in production (bot protection no longer fails open on a forgotten key); don't load .env under NODE_ENV=test so the suite is hermetic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eLegal Software
All-in-one practice management for law firms. Single Node app that serves both the React SPA and the API on one port — designed to run behind Plesk's Node.js extension on a single domain.
Stack
- Frontend — Vite + React 18 + TypeScript + Tailwind + Framer Motion + Recharts + lucide-react
- API — Fastify 5 + TypeScript + Zod
- DB — Drizzle ORM → DigitalOcean Managed Postgres (TLS)
- Auth — local: argon2id passwords + Postgres-backed sessions in httpOnly cookies (no third-party auth provider)
- Storage — DigitalOcean Spaces (S3-compatible, presigned uploads)
- Email — Resend
- Payments — Stripe
- Hosting — Plesk + Phusion Passenger (Node 20 LTS)
Repository layout
.
├── apps/
│ ├── api/ # Fastify server (also serves built web/dist in prod)
│ └── web/ # Vite + React SPA
├── packages/
│ └── db/ # Drizzle schema + migrations (shared)
├── certs/ # DO Postgres CA cert (do-ca.crt) — not in git
├── scripts/
│ └── plesk-deploy.sh
├── tmp/restart.txt # touched by deploy script to bounce Passenger
└── app.js # Plesk entrypoint (loads apps/api/dist/server.js)
Local development
Prerequisites: Node 20+, pnpm 9+, a Postgres database (managed DO instance, or local).
cp .env.example .env
# fill in DATABASE_URL, SESSION_SECRET, CSRF_SECRET (generate with `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`)
pnpm install
pnpm db:generate # generate SQL migrations from schema
pnpm db:migrate # apply to the DB
pnpm dev # starts api on :8080 and web on :5173 (proxies /api → :8080)
Visit http://localhost:5173.
Production build
pnpm build # builds packages/db → apps/web → apps/api
pnpm start # runs node app.js → apps/api/dist/server.js
The API serves apps/web/dist at / with SPA fallback and routes /api/* to Fastify handlers.
Plesk deployment (single domain)
- Create the domain in Plesk and enable Let's Encrypt TLS.
- Install Node.js extension (Plesk → Extensions → "Node.js"). Set Node version to 20.x in the domain's Node.js settings.
- Pull the repo into the domain's document root via Plesk → Git, or
git cloneover SSH into/var/www/vhosts/yourdomain.com/httpdocs. - Node.js settings in the Plesk panel for that domain:
- Application root → the repo root
- Document root → leave as default; nginx will proxy to Passenger
- Application startup file →
app.js - Custom environment variables → set every entry from
.env.example(Passenger does not read.envfiles)
- Add DigitalOcean's Postgres CA to
certs/do-ca.crt(download from the DO Postgres dashboard) and setDATABASE_CA_CERT_PATH=./certs/do-ca.crt. - Run the deploy script over SSH:
This installs deps, builds, runs migrations, then
bash scripts/plesk-deploy.shtouch tmp/restart.txtto bounce Passenger. - Stripe webhook — add
https://yourdomain.com/api/stripe/webhookin the Stripe dashboard. In Plesk → Apache & nginx → "Additional nginx directives" add:location /api/stripe/webhook { proxy_request_buffering off; } - Auto-deploy on push (optional) — in Plesk → Git, enable "Enable additional deploy actions" and set the script to
bash scripts/plesk-deploy.sh.
Environment variables
See .env.example for the full list. Highlights:
| Var | Purpose |
|---|---|
DATABASE_URL |
DO Managed Postgres connection string (?sslmode=require) |
DATABASE_CA_CERT_PATH |
Path to DO CA cert (recommended for rejectUnauthorized: true) |
SESSION_SECRET |
32+ byte hex used to sign cookies and as Fastify cookie secret |
CSRF_SECRET |
32+ byte hex for CSRF token derivation |
SPACES_* |
DigitalOcean Spaces credentials + bucket |
RESEND_API_KEY |
Transactional email |
STRIPE_* |
Billing |
PORT |
Port for Fastify (Plesk usually injects this; falls back to 8080) |
COOKIE_DOMAIN |
Set to your apex domain in production (e.g. lawdesk.com); leave blank in dev |
Database commands
pnpm db:generate # create a new migration from schema changes
pnpm db:migrate # apply pending migrations
pnpm --filter @lawdesk/db studio # open Drizzle Studio
Auth model
- Passwords hashed with argon2id (64MB memory cost).
- Cookie holds a 32-byte random token; the DB stores its SHA-256 hash (so a DB read can't impersonate users).
- Sessions are 30-day sliding (touched on every request).
- Login rate limited: 5 failed attempts per email per 15 minutes.
- All
/api/*requests automatically attachreq.userif a valid session cookie is present. Useapp.requireAuth/app.requireFirmas preHandler guards on protected routes.
What's next
- Wire DO Spaces upload routes for documents
- Build the
/appdashboard (cases, time tracking, invoices) - Free public tools (
/tools/*) - Stripe checkout + webhook
- Email templates via Resend
- pg-boss background jobs
Languages
TypeScript
98.6%
Dockerfile
0.5%
JavaScript
0.3%
HTML
0.3%
CSS
0.2%