310568690b40b98572d82ba645e1784d9edc995e
- New NavDrawer + hamburger menus for the app and admin layouts (below md there was previously no navigation at all) - Disable the dead Documents nav link that routed to the marketing landing page via the catch-all route - Replace the decorative topbar search with a debounced clients+cases search dropdown (keyboard navigable, loading/error/empty states) - Surface dashboard API failures: retry banner, em-dash KPIs, and a retryable error state on the recent-cases card 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%