← Back to projects
Full-Stack · Work in progress
FreelanceOS
Full-stack freelance management platform built with Next.js 16 and Drizzle ORM over Neon (serverless PostgreSQL). Tracks clients, projects, time entries, and invoices — with double-billing prevention enforced at the database layer and atomic invoice creation via transactions.
Demo account: demo@example.com / password123. Instant access to 255+ pre-seeded records.
Stack
- Next.js 16
- TypeScript
- Drizzle ORM
- Neon
- PostgreSQL
- Tailwind CSS
Overview
- Double-billing prevention enforced at the DB layer: a foreign key on time_entries.invoice_id means the database rejects assigning a time entry to a second invoice before application logic runs
- Atomic invoice creation via db.transaction() — line items, totals, and the invoice header are committed together or rolled back entirely; partial invoices cannot exist
- Overdue status derived at query time (dueDate < NOW()) rather than stored as a flag — no background sync job, no stale data
- Dashboard KPIs computed with SQL-level aggregations (SUM, COUNT CASE WHEN) directly in Drizzle queries — no in-memory accumulation on the server
- 255+ pre-seeded demo records for instant evaluation without sign-up friction, seeded via an idempotent script
Screenshots

Authentication entry point with a one-click 'Try Demo Account' button that pre-fills credentials and signs in immediately — no sign-up flow required to evaluate the app with 255+ pre-seeded records.
1 / 6
Architecture
- Next.js 16 App Router with Server Actions for all data mutations — no separate REST or RPC layer
- Drizzle ORM over Neon (serverless PostgreSQL) — schema definitions in TypeScript propagate DB-level constraints (foreign keys, NOT NULL) into the type system
- Invoice creation wrapped in db.transaction(): invoice header, line items, and total are committed atomically or rolled back entirely
- Foreign key on time_entries.invoice_id → invoices.id prevents double-billing at the database level — no application-level lock or check required
- Overdue status derived at query time via dueDate < NOW() — never stored as a boolean flag that could become stale
- Dashboard aggregations pushed into SQL (SUM, COUNT CASE WHEN) to avoid loading full record sets into application memory
Engineering Challenges
- Preventing double-billing without application-level locking: the DB foreign key constraint on time_entries.invoice_id is the enforcement boundary — any second assignment is rejected before the application layer sees it
- Keeping invoice totals consistent without triggers or sync jobs: line item amounts are computed from time entries at creation time and committed atomically, so the invoice total is always authoritative
- Avoiding stale overdue state: storing is_overdue as a boolean requires a background job to flip it; deriving it in every query from dueDate < NOW() eliminates that failure mode entirely
- Seeding 255+ realistic interrelated records (clients, projects, time entries, invoices) while keeping the seed script idempotent across repeated runs
What I'd improve
- Add server-side PDF export for invoices using a React-to-PDF rendering pipeline
- Implement recurring invoice templates to reduce manual entry for repeat clients
- Introduce row-level security on Neon to isolate tenant data at the database layer
- Add payment status webhooks for automatic reconciliation against Stripe events




