Pricing Sheets
My own SaaS. Instead of a PDF quote, the client gets a live proposal: they pick a package, switch add-ons on and off, watch the total update, and sign on the same page.
- Role
- Founder and sole developer
- Timeline
- 2026
- Stack
- Next.js, FastAPI, PostgreSQL with row-level security, a Yjs collaboration service, Hetzner

The situation
Agencies and IT providers still send quotes as PDFs. The buyer can't try a different package or drop an add-on, so every change is another email and another PDF.
I built Pricing Sheets to compete with tools like Smart Pricing Table and PandaDoc: a proposal the buyer can actually use.
What I built
- Multi-page proposals and an interactive buyer view: packages, add-ons and a total that updates as they choose.
- Multi-currency, conditional pages that show only when they apply, and a reusable clause library.
- Multi-signer e-signature with a hash-chained audit trail and a completion certificate.
- PDF export and per-page analytics, so the seller sees what the buyer actually read.
- A public REST API with API keys, HMAC-signed webhooks, and Slack and Zapier hooks.
- Website-to-brand onboarding: paste a website and the proposal picks up its logo and colours.
- One pricing engine written twice, in Python for the server and TypeScript for the browser, kept identical by shared test files.
- Real-time collaborative editing through a Yjs service, with row-level security enforced in PostgreSQL.
- AI drafting and pay-on-accept are built and being switched on.

What was hard
A sign-in that failed across two domains
Google sign-in returned a configuration error. It started on the marketing domain and came back to the app domain, so the session cookie never matched. I moved the whole sign-in onto the app domain.
A demo that broke after one use
The public demo proposal could be accepted, and once it was, it stayed accepted for every visitor after. I made it non-consumable so every visitor gets a fresh one.
A website fetcher that could be pointed inward
The brand onboarding fetches any website a user pastes. I closed a DNS-rebinding gap so it can never be tricked into reaching internal addresses.