← 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

FreelanceOS login page

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

Links