Key takeaways: A thorough brief still leaves product rules undefined, usually wherever a number or a default is implied without being written down. Settle those in writing before any code. Keep a parked list and take it as seriously as the feature list. An integration can be sequenced with an interface and a stub, so the rest of the product doesn't wait on it. Expect to reverse a few decisions later. One of our nine already has been.
The Brief We Were Handed
Receivix is an invoicing and collections dashboard for marketing agencies that we're building for a client. The case study covers the product. This post is about what happened between receiving the client's brief and writing the first feature.
The brief was good. It ran to twelve sections. It described the buyer with numbers: founder-led agencies with two to twelve staff and eight to twenty active clients, each client paying somewhere between $1,000 and $10,000 a month. It organised the product around the two offers it would be sold on, Get Paid Fast and Never Chase. It had a diagram of invoice statuses, and it had a section listing features that were deliberately left out of the first version.
We still couldn't start building from it, and that isn't a criticism. A brief describes what a product does. Code also needs to know what happens in every case the description skips, and nobody writing a brief can see all of those from where they sit. Reading it closely turned up nine questions that needed an answer first.
The Nine
| What the brief left open | What we settled |
|---|---|
| Agencies have a cap on active clients, called N. What is N? | 20 by default, stored per agency, and the owner can change it |
| Late fees are charged per month overdue. How is a part-month counted? | Every started 30 days counts as a month, after any grace period |
| Do late fees apply on their own? | A per-agency setting, and by default the owner approves first |
| How does tax treat ad spend? | One agency rate with a per-client override. Fee and other lines are taxed, ad spend only if that client is flagged |
| Which accounting system comes first? | QuickBooks Online, as an interface and a stub, with no live connection in version one |
| What about currencies? | US dollars by default, with the currency recorded on the agency and on every invoice |
| Sign-in and tenancy | better-auth with its organizations feature, chosen over Auth.js and Clerk |
| Database | PostgreSQL, hosted by the client in production |
| Package manager | pnpm |
The first four are product rules and the next two are about sequencing. The last three were ours to make, and the client mostly needed to know they had been made.
A Number That Wasn't There
The brief gave every agency a limited number of client slots and called that number N. It's an easy thing to leave as a letter, because the idea is perfectly clear without it. A database column needs a value though, and so does the screen that tells an owner they're full.
Nobody tried to work out the one correct number. The answer was a default of 20, which matches the top of the client range in the brief, stored on the agency record where the owner can edit it. That's how most of these end. Somebody picks a sensible default and the number becomes a setting.
What Counts as a Month
The brief said late fees could be a percentage per month overdue. It didn't say what an invoice 29 days late owes, and there are two defensible readings. Count only completed months, and on a $5,000 invoice at 1.5% that client owes nothing yet. Count every started month, and they owe $75. At 31 days the two readings give $75 and $150.
Either could be right. What matters is that the agency's clients will notice which one they got, so it can't be left to whoever happens to write the function. We settled on every started 30 days counting as a month, with any grace period taken off first. It is one line in the spec and it's now a unit test. The engineering side of late fees is in the collections post.
Defaults Are Decisions Too
The brief wanted late fees, and it wanted approvals for sensitive actions. It didn't say whether adding a late fee counts as one of those actions.
That matters more than it looks. A fee that appears on a client's invoice overnight is something the client will react to, and the brief's whole audience is founders who manage those relationships personally. So late fees became a setting, and the default is that the first fee on any invoice waits for the owner to approve it. An agency that wants it automatic can turn the approval off. Final-notice emails wait for approval by default as well.
Tax was the same kind of gap. Ad spend is the client's own budget passing through the agency, and whether it should carry tax isn't something a product can decide for every agency in every place. Tax applies to the management fee and to other lines, and ad spend is taxed only for clients where the agency switches that on.
First Integration, Without the Integration
The brief asked for accounting sync and named more than one system. Live connections to all of them were never going to fit in a first version.
So the decision was QuickBooks Online first, and for version one only the shape of it: an interface with four operations (push an invoice, push a payment, push a client, test the connection), a queue, a sync log, and a stub provider on the other end. Sending an invoice or recording a payment already queues a sync. When the live connection is built it replaces the stub, and nothing in invoicing or payments has to change.
Currency got a lighter version of the same treatment. Everything is US dollars for now and there is no conversion. The currency is still recorded on the agency and on each invoice, because adding that column later to a table full of real invoices is a much worse job than carrying it from the start.
The Parked List
The most useful section of the brief may have been the one about what not to build.
It parked online payments, SMS and WhatsApp reminders, tracking whether a client has viewed an invoice, webhooks, a payment-terms calculator, and legal or contract content. We added a few of our own while writing the spec: live accounting connections, email open tracking, currency conversion, row-level security in the database, and password-reset flows beyond what the auth library does out of the box.
Parked doesn't mean forgotten, and you can see that in the data model. Each invoice has a public token even though nothing tracks views yet. The payment method field already has a card option although no card processor is connected. The accounting provider is an interface. We kept those three because leaving them out would have been expensive to undo, and added nothing else for features that aren't in scope.
One Document Wins
All nine answers went into a written spec, along with the data model, the routes, and a short list of rules the code has to follow, such as money always being stored as whole cents. Near the top of the spec is a line saying that where it and the brief disagree, the spec wins.
It sounds like a formality. Without it there are two descriptions of the product, and each disagreement between them gets settled by whoever happens to be building that part.
This is the scoping work we described in the milestone pricing post. It happens before a price exists, and the acceptance criteria for each milestone come out of it.
What We've Already Changed
One of the nine has been reversed. The spec says pnpm and the project runs on bun now. It was a tooling choice with no effect on the product, so it cost very little to change.
We'd expect a couple more to move once real agencies are using it. The slot default and the months rule are both judgment calls, the product isn't live yet, and no customer has pushed back on either. Each one is a setting or a single function, written down in one place, so changing it is a small job.
If You're Writing a Brief
You don't need to answer all of this yourself. It helps a great deal if the brief makes the gaps easy to find. Going by this one:
- Describe the buyer with numbers. Team size, client count, and price range make it possible to pick sensible defaults.
- Wherever a rule has a number in it, write the number or write "undecided". A cap of N and a fee per month both hide a question.
- For every automatic action, say whether a person approves it first. Especially anything your own customers will see.
- Include a parked list. It stops the first version growing, and it tells the developers which doors to leave open.
- Say which integration comes first. The second and third can wait behind an interface.
- Draw the states. The status diagram in this brief became the state machine in the code.
If you have a brief and want to know what it leaves open, tell us what you're building.