case studyChronoWorks

Time, tasks and invoices for freelancers, where the database is the referee.

Open-source MVP, self-hosted · by Firas Ridene

ChronoWorks is one place where a freelancer runs the business side of their work: clients, projects, time, tasks, leave and invoices, all linked. It is built for Tunisia and France first (TND and EUR, matricule fiscal and SIRET, stamp duty, withholding tax, EU reverse charge). Row-level security, constraints and triggers in Postgres enforce the rules, so a bug in the UI can't produce a wrong invoice or leak another user's data.

Type
Open-source MVP, deployable to your own Supabase and Vercel
Markets
Tunisia and France, French and English interface
Stack
Next.js 16 (App Router), TypeScript, Tailwind v4, Supabase (Postgres, Auth, Storage), dnd-kit, @react-pdf/renderer, next-intl

Problem and target user

A freelancer working alone usually ends up with a spreadsheet for hours, a kanban tool for tasks, and a word processor for invoices. Nothing knows about anything else, so the same hour gets typed three times, and the invoice is assembled by hand at the end of the month, from memory.

ChronoWorks makes those one system. A task has a log-time button, time becomes invoice lines, and issuing an invoice locks the time behind it.

Constraints and requirements

  • An issued invoice must never change afterwards: not its fields, its lines, or the time entries behind it.
  • Invoice numbers must be gapless per year.
  • Raising a rate or editing a client's details later must not rewrite invoices already sent.
  • Money in two currencies with different precision: TND with three decimals, EUR with two.
  • One user's data must be unreachable by any other user or by a signed-out visitor.

Architecture and engineering decisions

The rule that shapes everything: the database is the referee, not the UI.

  • Every table has row-level security scoped to the signed-in user. Child rows use composite foreign keys, so a row can't reference another account's data even by accident.
  • A trigger freezes an issued invoice, its lines and the time entries behind it.
  • issue_invoice() is the only way to issue an invoice. It assigns the gapless number, recomputes totals from the lines, and snapshots both parties' billing details.
  • A project's rate is copied onto each invoice line when the line is created, so a rate change next year can't alter last year's invoice.
  • Money is numeric in Postgres and a scaled bigint in code, never a float. Durations are whole minutes everywhere.
  • Leave is drawn on the week grid and excluded from working-day counts.

My contribution

I designed and built it: the data model, the Postgres migration with its triggers and RLS policies, the Next.js app, the PDF invoices, and the verification and seed scripts. All commits in the public repository are mine.

Verified outcomes

  • An RLS test script creates two throwaway users, has one build a full workload, then tries every way it can to reach that data as the other user and as a signed-out visitor. Result: 46 checks, 0 failed.
  • A verify script checks the live Supabase project: Google sign-in enabled, anonymous access refused on all 12 tables, and the functions and private storage buckets present.
  • The MVP scope is built: auth, clients, projects with status history, the week grid and timer, leave, boards, invoices with PDF and storage, settings, GDPR export, and account deletion.

Limitations

  • It's an MVP, not a hosted service. You deploy it to your own Supabase project and Vercel account.
  • Sign-in is Google only.
  • Out of scope for v1: dashboards and revenue charts, quotes, expenses, recurring projects, invoice reminders, sending email, accountant export, Arabic and RTL, and teams.

Source and demo

Want something like this for your business, or hiring for an AI engineering role? I'm Firas Ridene, AI Engineer in Tunis, Tunisia, focused on Generative AI & Multi-Agent Systems.