Prismo
A freelance management platform built solo: client and project management, time tracking, and invoicing in one system. Built for technical freelancers who spend more time on admin than they should.

Summary
A freelance management platform built solo: client and project management, time tracking, and invoicing in one system. Built for technical freelancers who spend more time on admin than they should.
- < €60Monthly infrastructure
- 6Core modules
- 7Architecture RFCs
- 40+API endpoints
The Problem
Most freelancers run their operations across four or five disconnected tools. When billing day arrives, they reconstruct last month from git history, timer exports, and scattered notes. The invoice goes out late, the hours do not quite add up, and the client asks questions.
The real cost is not the missed hours. It is the time spent on administration that should have been billable, and the under-confidence that follows when work cannot be reconstructed precisely enough to defend.
Prismo is one system from the first hour logged to the invoice paid. No data re-entry, no end-of-month reconciliation.
The Approach
Built solo: product definition, seven architecture RFCs, Go backend with Hexagonal Architecture, Next.js frontend, PostgreSQL schema with Row-Level Security, Supabase Auth, and deployment across Fly.io and Vercel.
Before writing a single migration, I documented the billing pipeline's non-negotiable constraints as formal RFCs. A freelance invoicing system has more invariants than it appears - contracts must not overlap in date ranges, invoice numbers must be gapless even under concurrent requests, time entries must lock when an invoice is sent and unlock only on void, and every billing detail must be frozen at generation time so historical invoices cannot be altered by later profile changes.
Getting those invariants right before touching the schema prevented the kind of retrofitting that would otherwise have cost weeks mid-build.
The Outcome
A production-grade freelance operations platform covering the full billing cycle: invite-only multi-tenant workspaces, client and project management with soft-archive controls, hourly and fixed-price contracts with versioning and overlap protection, time tracking with task-assignment enforcement, atomic invoice generation with full data snapshots and gapless INV-XXXX numbering, client-facing share links requiring no client login, and asynchronous PDF generation.
Running at under €60 per month in infrastructure. Invite-only beta open at getprismo.app.
Four Invariants I Would Not Compromise On
Before writing a migration file or a route handler, I documented four constraints the system had to satisfy regardless of how everything else was designed.
Invoice numbers are gapless. INV-0001, INV-0002, INV-0003 - no gaps, no repeats, even under concurrent generation requests from the same workspace. Skipped numbers invite questions from clients and tax authorities that nobody wants to answer mid-engagement.
Time entries lock when an invoice is sent. The hours on a sent invoice are a commitment. Once a client has seen the total, the freelancer cannot go back and edit a logged entry. Voiding the invoice is the only path to unlocking them.
Invoices snapshot their data; they do not query it live. Once generated, an invoice freezes every billing-relevant field: workspace name, billing address, client details, project name. If the freelancer rebrands six months later, historical invoices still show the name that was correct when issued. Rendering from live profile data makes invoices legally unreliable.
Unassigned time blocks billing. Every billable hour must be attributed to a task before it can appear on an invoice. If a client disputes a line, the freelancer must be able to explain exactly what those hours covered. The Unassigned Inbox surfaces every floating entry; generation will not proceed until they are all assigned.
Those four constraints shaped every subsequent architecture decision.
System Architecture
Prismo runs on a deliberately small stack: Next.js on Vercel for the frontend, a Go API on Fly.io, and PostgreSQL via Supabase for the database, authentication, and file storage. Total infrastructure cost at current scale is under €60 per month.
flowchart LR FE["Next.js (Vercel)"] API["Go + Fiber (Fly.io)"] DB[("PostgreSQL")] AUTH["Supabase Auth"] STORE[("File Storage")] FE --> API API --> DB API --> AUTH API --> STORE
The backend is structured around Hexagonal Architecture - ports and adapters. Every external dependency connects through a defined interface; the domain layer carries no direct infrastructure dependencies. Invoice pricing logic can be tested in isolation from day one, and adding or swapping infrastructure adapters requires no changes to the core business logic.
Multi-Tenant Isolation
The workspace is the isolation unit. Every table carries a workspace identifier, and Row-Level Security enforces tenant boundaries at the data layer - not just in application middleware. A query from one workspace cannot return rows from another, regardless of how the request is constructed.
The membership model was built in v1 even though the product initially exposes a single workspace per user. Treating the workspace as a join from day one means multi-workspace support and team invites can be added without touching the schema.
Access Control at the Identity Boundary
Beta access is invite-only. The enforcement runs before a user identity is created, not after. An identity-layer hook fires before any account row is written; unapproved email addresses are rejected before they exist in the system. By the time an application-layer check runs, the user already exists in the identity store - too late to enforce a meaningful boundary.
Contract Versioning
Billing terms change over the life of an engagement. Prismo models this as versioned contracts: each contract carries an effective date range, and a new contract automatically closes the previous one the day before the new terms start. A guard prevents this adjustment if the period being truncated has already been invoiced.
At invoice generation time, each time entry is priced against the contract that was effective on that entry's date. A billing period spanning a rate change produces two line items - one at each rate - calculated correctly without any manual adjustment. Renegotiating a rate mid-project is a normal part of freelancing; the system should handle it without requiring the freelancer to do arithmetic.
The Billing Pipeline
flowchart TD A[Log time entry] --> B{Task assigned?} B -- No --> C[Unassigned Inbox] C -- Assign task --> B B -- Yes --> D[Billable pool] D --> E[Preview invoice] E --> F{Blockers cleared?} F -- No --> C F -- Yes --> G[Generate invoice] G --> H[Draft] H --> I[Send to client] I --> J[Entries locked] J --> K[Mark as paid]
The preview runs the full pricing calculation - multi-contract grouping, unbilled entry totals, all of it - and returns a complete line-item breakdown without writing anything to the database. The freelancer sees exactly what will be generated before committing.
The generate action is the only step with side effects: it increments the invoice counter atomically, snapshots all billing-relevant data, creates the invoice and its line items in a single transaction, and enqueues the PDF job after the commit. If any step fails, nothing is created and the counter is not incremented.
Time entry locking happens at Sent, not at Generate. A draft invoice can still be voided cleanly - no entries are affected. Once sent, the entries lock; voiding the invoice unlocks them for re-use in a corrected invoice.
stateDiagram-v2 [*] --> Draft : Generate Draft --> Sent : Send to client Draft --> Void : Cancel draft Sent --> Paid : Payment confirmed Sent --> Void : Dispute
What Shipped
| Module | What it delivers |
|---|---|
| Workspace & Identity | Passwordless sign-in via Google OAuth and magic link. Invite-only access enforced before any account is created. Idempotent workspace provisioning - safe to call on every login from any device. Onboarding state derived from live business data, never from flags that can drift. |
| Client Management | Create a client with a name only; billing details enriched progressively before the first invoice. Per-client currency override with workspace fallback. Archiving a client with outstanding invoices is blocked; archiving cascades to all underlying projects. |
| Project Management | Projects carry an explicit status - active, paused, or completed - with enforced transition rules. Pausing blocks new time entries. Contracts version under each project with automatic date-overlap protection and immutability once invoiced. |
| Time Tracking | Every billable hour belongs to a project and a task. The Unassigned Inbox is a first-class feature: a workspace-wide queue of entries without task attribution. Any entry left there blocks the next invoice. |
| Invoicing | Preview with no side effects, generate with atomic counter and full data snapshot, four-state lifecycle, time entry locking on send, unlocking on void, client-facing share link with optional expiry, and asynchronous PDF to file storage. |
| Workspace Profile | Legal name, billing address, and currency stored at the workspace level and snapshotted into every invoice at generation time. Changes to the profile apply forward only; historical invoices are immutable. |
Invite-only beta is open. If you are a solo technical freelancer spending more time on administration than on the work itself, the waitlist is at getprismo.app.
Impact
- < €60Monthly infrastructure
- 6Core modules
- 7Architecture RFCs
- 40+API endpoints
Learnings
- 01
Writing data model invariants as RFCs before any migration prevented the expensive schema rewrites that follow early-stage shortcuts.
- 02
Snapshot immutability is non-negotiable for invoices. Rendering from live profile data makes historical invoices legally unreliable.
- 03
Atomic counter increments - handled within the generation transaction itself - are simpler and more reliable than any separate sequence management.
- 04
Access control belongs at the identity boundary, not the application layer. By the time an application check runs, the user already exists in the identity system.
- 05
Building a tool for freelancers whilst freelancing compresses the feedback loop to nothing. Every design decision has a real consequence the same day.
Got something to build?
Drop me a line or book a short call. Replies are usually back the same day.
Let's talk →