4.3 · Product Spec to Technical Spec

4.3 · 25 min

Product Spec to Technical Spec

Somewhere around the fourth dispatch of a capstone-sized build, an agent will need to know whether the thing a client approves is called a deliverable or an asset, whether approval is a status or a table, and whether clients belong to projects or to orgs. If those answers live in your head, every session re-derives them, each slightly differently, and by dispatch ten your codebase has deliverables, assets, and a files table that are the same concept wearing three names. The technical spec exists to answer every question of that shape exactly once, in writing, before any dispatch runs.

Two specs, two jobs

Lesson 3.1 named the altitudes; this lesson writes the middle two for Briefcase. The product spec refines the brief into observable behavior: who can do what, and what they see happen. The technical spec compresses that into buildable truth: entities with names, routes with paths, a permissions matrix, and the list of external systems. The direction of authority runs downward: the technical spec must trace to the product spec, which must trace to the brief (4.1), and every dispatch in 4.4 through 4.10 names this document as its source. When a dispatch and the spec disagree, the spec wins or the spec gets amended first, on an operator decision, never silently mid-dispatch.

What makes a technical spec good is not completeness, it is decidability. For any question an agent will hit while building ("what are the role names?", "can a member delete a project?"), the spec either answers it or explicitly marks it open. Ambiguity in this document is not neutral; 1.4 told you what fills unspecified gaps, and here a filled gap becomes a schema you live with.

The reference product spec

briefcase/docs/PRODUCT-SPEC.mdmarkdown
# Briefcase product spec (v1)

Traces to: docs/BRIEF.md. Amend only by operator decision.

## Actors
- Agency staff: owner, admin, member (of an org)
- Client: external user invited to specific projects only

## Core behaviors
1. A user creates an org (their agency) and becomes its owner.
2. Staff invite users: staff roles join the org; client users are
 additionally granted access to named projects only.
3. Staff create projects and upload deliverables to them.
4. A deliverable moves: draft -> in_review -> approved OR
 changes_requested. Staff move draft -> in_review. Clients (or
 staff) decide in_review -> approved / changes_requested.
5. Anyone who can see a deliverable can comment on it.
6. Every approval decision writes an immutable audit record: who,
 which deliverable, decision, note, when. Records are never
 updated or deleted, including by staff.
7. Each user receives at most one digest email per day summarizing
 comments and decisions on things they can see. No digest if
 nothing happened.
8. An org has one subscription ($49/mo, 14-day trial). When it
 lapses, staff can read everything but writes are blocked;
 clients are unaffected for 30 days (grace), then read-only too.
9. Org billing is managed by owner/admin via a hosted portal.

## Visibility invariant (the product, in one line)
A user sees an org's data only if they are a member of that org,
and a client member sees only projects granted to them. No
exception exists anywhere in v1.

## Explicitly out (from the brief, restated as behavior)
No public links. No cross-org anything. No file versioning UI
beyond a version counter. No per-deliverable permissions. No
realtime. No search (A8).

Behavior 8 is the kind of decision that hides until an agent has to guess it: what does a lapsed subscription actually do? Deciding "reads stay, writes block, clients get grace" took one minute here and would have cost a redesign in week three. Walk the spec looking for these: every state a subscription, deliverable, or membership can be in must have a defined behavior, or a dispatch will define it for you.

The reference technical spec

briefcase/docs/TECH-SPEC.mdmarkdown
# Briefcase technical spec (v1)

Traces to: PRODUCT-SPEC.md. Architecture: ADR-001..003.
This file is the naming authority. Dispatches use these names.

## Entities (Postgres, all tenant tables carry org_id; ADR-001)
- orgs: id, name, slug, created_at
- org_members: org_id, user_id, role
(owner | admin | member | client), created_at
- projects: id, org_id, name, status (active | archived), created_at
- project_access: project_id, user_id, created_at
(client visibility grants; staff need no rows here)
- deliverables: id, org_id, project_id, title, file_path, version,
status (draft | in_review | approved | changes_requested),
created_by, created_at, updated_at
- comments: id, org_id, deliverable_id, author_id, body, created_at
- approvals: id, org_id, deliverable_id, decided_by, decision
(approved | changes_requested), note, created_at  [append-only]
- subscriptions: org_id (pk), stripe_customer_id,
stripe_subscription_id, status (trialing | active | past_due |
canceled | incomplete), price_id, current_period_end, updated_at
- webhook_events: id (stripe event id), type, received_at,
processed_at  [service-role only]
- jobs: id, kind (notification_digest | file_processing), payload,
idempotency_key, status (pending | running | succeeded | failed |
dead), attempts, max_attempts, run_after, last_error, created_at,
updated_at  [service-role only]

## Routes
Pages: /login, /signup, /invite/[token], /dashboard,
/projects/[projectId], /deliverables/[deliverableId],
/settings/members, /settings/billing
Route handlers:
- POST /api/stripe/checkout   (create checkout session)
- POST /api/stripe/portal     (create portal session)
- POST /api/stripe/webhook    (billing truth; signature-verified)
- POST /api/jobs/run          (worker; cron-swept + direct kick)
- POST /api/invites           (create invite)
All other data access: server components and route-handler queries
through the Supabase client under RLS. No separate REST layer.

## Permissions matrix (RLS is the enforcement point; 4.5)
action                        owner admin member client
create/rename org               Y     Y     -      -
manage members + invites        Y     Y     -      -
create/archive project          Y     Y     Y      -
grant client project access     Y     Y     -      -
upload deliverable, set
in_review                     Y     Y     Y      -
approve / request changes       Y     Y     Y      Y*
comment                         Y     Y     Y      Y*
read project/deliverables       Y     Y     Y      Y*
manage billing (portal)         Y     Y     -      -
read billing status             Y     Y     -      -
(* = only on projects granted via project_access)

## Integrations
- Supabase: auth (@supabase/ssr), Postgres + RLS, storage
(bucket: deliverables, path <org_id>/<deliverable_id>/<version>)
- Stripe: Checkout, customer portal, webhooks (events in 4.7)
- Email: one transactional provider for digests, chosen at build
time after checking current options (4.8); abstracted behind
lib/email.ts sendEmail() so the choice is one file.
- Vercel: hosting, cron (worker sweep), preview deploys

## Open questions (operator decides before the relevant dispatch)
- none currently

Three properties of this document are the ones to copy, beyond its content. It is short: two screens, readable by an agent in one gulp alongside a dispatch, cheap to keep loaded in every session (1.3's economics). It is decisive: role names, status enums, route paths, and the storage path scheme are settled here, so no dispatch ever invents one. And it has an open-questions section, currently empty, which is where ambiguity goes to be seen instead of being resolved by whichever session hits it first.

The permissions matrix deserves a sentence of defense, because it looks bureaucratic and is actually the most load-bearing block in the file. It is the visibility invariant made checkable: 4.5 turns each cell into an RLS policy, and each Y/dash pair into a policy test. When lesson 4.6 stages the classic client-side-auth attack, the matrix is the document the attack is checked against. A permissions model that exists only as vibes cannot be attacked, which is precisely what is wrong with it.

Scoping v1 ruthlessly

The in/out list from the brief gets one more pass here, at technical altitude, because some scope only becomes visible when you name entities. Writing deliverables.version surfaced a question the brief never had to answer: is version history a v1 feature? The spec's answer is a counter and nothing else: uploading again increments version and replaces the file path; there is no history UI, no diffing, no restore. That single sentence deleted perhaps a week of dispatches. Do this deliberately: for each entity, ask what is the least this can mean and still serve the product spec's behaviors, and write that meaning down. The out-list is not just features you declined; at this altitude it is capabilities your entities deliberately do not have.

Where it breaks

The first failure is the spec that is really a wish: entities named but relationships undecided ("clients are associated with projects somehow"). Every "somehow" is a dispatch-time guess wearing spec clothing. The test for doneness is mechanical: could two different agents, in two different sessions, produce compatible migrations from this document? If anything is left to interpretation that matters, it is not done.

The second is the over-spec: fifteen pages, every component enumerated, API shapes for endpoints that will not be built for a month. Past the point of decidability, detail rots (the doc drifts from the code) and bloats context. The spec pins names, boundaries, and invariants; dispatches carry the per-session detail. If a section will not be read by any dispatch in the next two weeks, cut it.

The third is spec drift with silent amendment: an agent hits a mismatch mid-dispatch (the spec says decided_by, the code says approved_by) and helpfully updates the spec to match the code. Now your naming authority is downstream of its subjects. The rule from 3.1 applies: the spec is operator-owned; agents log mismatches to open questions, and you amend. Put that sentence in the capstone repo's CLAUDE.md in 4.4.

Lablab-4-3Write both specs, then diff against the reference

Goal: Produce a product spec and technical spec for Briefcase from the 4.1 brief, then reconcile against the reference specs and adopt a final pair for the build.

Prereqs: the Briefcase brief (4.1), your ADRs from lab 4.2, briefcase-notes folder.

  1. From the brief alone (do not scroll up), write product-spec.md: actors, numbered core behaviors, the visibility invariant in one line, and the out-list restated as behavior. Force yourself to answer the lapsed-subscription question and the who-can-approve question explicitly.
  2. Write tech-spec.md: entities with column-level names and enums, routes, the permissions matrix (draw the full grid), integrations, open questions. Where you catch yourself writing "somehow" or "TBD", either decide now or move it to open questions deliberately.
  3. Run the decidability test with an agent: paste tech-spec.md into a fresh session with the prompt: "You are about to implement this spec. List every question you would have to guess an answer to, ordered by how expensive a wrong guess would be. Do not answer them." Fold each real gap back into the spec.
  4. Diff both files against the reference specs in this lesson. For every difference in naming or shape, pick one deliberately. Then adopt the reference specs verbatim for the rest of the track (the dispatches in 4.4 through 4.10 use these exact names); keep your versions as the template for your own product.
  5. Commit briefcase-notes with all four documents.

Verify

  • Your product-spec.md defines behavior for every subscription state and names who can move a deliverable through each status transition.
  • Your tech-spec.md has a full permissions grid (every action/role cell filled with Y or a dash, no blanks) and zero instances of TBD outside the open-questions section.
  • The decidability run produced at least two real gaps on your first pass (if it produced zero, check whether your prompt let the agent be polite), and the final spec answers or explicitly parks each one.
  • The reference specs are in your folder as the build's naming authority.
>Troubleshooting
  • The agent's gap list is padded with hypotheticals (what about GDPR, what about i18n): rank order was the defense; take the top expensive gaps seriously and log the long tail to open questions or the out-list. A gap that does not block a v1 dispatch is not a spec bug.
  • Your permissions matrix and your prose disagree (prose says clients comment, grid says dash): the grid wins, fix the prose. Two representations of one invariant must not diverge in the very document meant to end divergence.
  • You cannot decide an entity question (should approvals reference a deliverable version?): decide the cheap-to-reverse version now (v1: no, approvals reference the deliverable; the audit note records what was seen) and record it in open questions with a revisit condition, the ADR move at smaller scale.

Knowledge check

Knowledge check

Q1Mid-dispatch, the agent reports: the tech spec calls the column decided_by but the migration written two dispatches ago used approved_by. It proposes updating the spec to match the code and continuing. What is the right call?
Q2Why does the permissions matrix belong in the technical spec rather than living implicitly in the RLS policies that enforce it?
Q3Your technical spec for your own product is up to twelve pages: every component, full API request/response shapes for all endpoints, error codes, and a state diagram per entity. What does this lesson predict?
Q4The product spec says a lapsed org's staff can read but not write, and clients keep full read access for 30 days. Where will this rule actually be enforced when the build is done, and why was deciding it now (rather than at build time) worth a lesson's attention?

Sources