Front page.What it is
Live
PSA
A money cockpit for a service business: quote, contract, invoice, payment and hours in one app, in French first.
Professional services automation. My company's quotes run on it today. The screens below are the public demo; the company and every number in it are invented.

Set in type.The stack
What it runs on
One Next.js app on the shared database. The tax and PDF code live in shared packages the other apps use too.
Interface
- Next.js16.2app and client portal
- React19.2
- Radix + shadcncustomized components
- Recharts2.15charts
Money
- Stripe20invoice payments
- GST + QSTboth taxes on every document
- PDF rendererquotes, contracts, invoices
- ExcelJSexports
Data
- Payload3.82records and admin
- Neon Postgresone shared database
- Drizzle0.45queries
- Better Auth1.5sign-in
Mail + host
- Handlebars + juiceemail templates
- Resenddelivery
- Vercelthe app and the demo
On the press.How it works
5 steps, in order
From a blank quote to money in the bank. Every screen is the public demo.
Build the quote
Four steps. Line items sit under features, priced in hours times a rate. The PDF draws itself beside the form as you type.

Pass the send gate
The quote cannot leave until it has a client, a project, a mandate, a payment method and a contract PDF.

One link for the client
They accept, sign the contract, fill in onboarding or ask for a revision behind one private link. Revision requests are sorted by AI.
Get paid
The invoice link opens a Stripe checkout. Payments land in Finances with GST and QST split out.

Clock the hours
Hours from the day card land against the quote's features, so the estimate and the actual sit side by side.

Pulled proof.The code
The send gate
export function evaluateSendGate(facts: SendGateFacts): SendGateResult { const missing: SendGateItem[] = [] if (!facts.hasClient) missing.push('client') if (!facts.projectName?.trim()) missing.push('project') if (facts.mandateCount < 1) missing.push('mandate') if (!facts.paymentMethod || !(PAYMENT_METHODS as readonly string[]).includes(facts.paymentMethod)) { missing.push('payment_method') } if (!facts.hasContractDraft) missing.push('contract') return { ok: missing.length === 0, missing }}Both ways of sending a quote call this one function, so no screen can skip it.
The reel.The walkthrough
Watch it run
Corrections.What it fixes
The problems it solves
Quotes built in a spreadsheet, outside any system.A four-step builder that draws the PDF as you type.
Contracts sent before anything was signed.Nothing leaves until the contract PDF is attached.
Unpaid invoices nobody sees until tax time.The overview opens on what is late and what is owed.
Fixed prices that never learn from real hours.Hours land on each feature, so the next quote starts from real numbers.
GST and QST worked out by hand.Both taxes are computed and split on every document.
Editor's letter.Why I built it
Why it exists
PSA began as a quoting tool for construction materials, priced like “14 tonnes of gravel at $42 a tonne.”
When my company moved to software work, the quotes became hours. A fixed price without measured hours is a bet you renew with every mandate, so the app learned to track time against what it quoted.

Against the rack.Better than average
Against the usual tools
Against Harvest and FreshBooks. Their side was read from each vendor's own help pages on 2026-09-27.
| What | The market average | This app |
|---|---|---|
| Client steps | One link to accept and e-sign; payment later, on a separate invoice. | One link: accept, sign, onboard, pay.vendor's page |
| Contract | A signature on the proposal (FreshBooks) or accept and decline only (Harvest). | A contract PDF drawn from the quote; nothing is sent without it.vendor's page |
| Quebec taxes | A tax rate you set up yourself. | GST and QST built in, with presets for other industries.vendor's page |
The imprint.How long it took
152 commits in 293 days
- commits
- 152
- days with a commit
- 39
- days, first commit to today
- 293
- First commit
- Moved from materials to software mandates
- Moved into the monorepo
- Latest commit
Measured: git log, my own commits on the app, 2026-09-27.
Colophon.Where it sits in the ledger
