Product Roadmap
Status: living document · Last updated: 12 June 2026 (RM-2 shipped)
Features we want to build, beyond what’s already committed in the MVP build order. The vertical slice and build order take priority — items here only get picked up when they fit the current phase.
How to add an item
Section titled “How to add an item”Add a row to the table under the right status, then a detail section below. Each item needs:
- ID — next integer (
RM-N). - Status —
proposed→planned→in progress→shipped(orparked). - Area — which workspace package(s) it touches (
app,web,db,api,infra). - A detail section with the problem, the rough shape of the solution, and any
open questions. No implementation detail until it moves to
planned.
When an item ships, move its row to the Shipped table and link the PR.
Proposed
Section titled “Proposed”| ID | Feature | Area | Added |
|---|---|---|---|
| RM-1 | Client verification code for quote link | web, db | 12 Jun 2026 |
| RM-3 | Invoice chasing with chase history | app, db | 12 Jun 2026 |
| RM-4 | Receipt capture with data extraction | app, db, api | 12 Jun 2026 |
| RM-5 | Suggested line items from past quotes | app, db | 12 Jun 2026 |
| RM-6 | Finance page (P/L, tax + NI estimate, CIS) | app, db | 12 Jun 2026 |
| RM-7 | Tax set-aside tracking | app, db | 12 Jun 2026 |
| RM-8 | Passwordless OTP email auth (Resend) | app, db | 12 Jun 2026 |
| RM-9 | Onboarding data capture + abuse mitigations | app, db | 12 Jun 2026 |
| RM-10 | Legal + compliance foundation | (business) | 12 Jun 2026 |
Planned
Section titled “Planned”| ID | Feature | Area | Target |
|---|---|---|---|
In progress
Section titled “In progress”| ID | Feature | Area | Branch/PR |
|---|---|---|---|
Shipped
Section titled “Shipped”| ID | Feature | Shipped | PR |
|---|---|---|---|
| RM-2 | Vectorised logos | 12 Jun 2026 | #38 |
RM-1 — Client verification code for quote link
Section titled “RM-1 — Client verification code for quote link”Status: proposed · Area: web, db · Added: 12 June 2026
Problem. The /q/:token quote page is protected only by the 32-hex
public_token in the link. Anyone who obtains the link (forwarded message,
shoulder-surfed screen, leaked chat history) can view and accept the quote as
if they were the client.
Shape. When a quote is sent, generate a short verification code alongside
the token and deliver it to the customer with the link (same message for now;
a separate channel could come later). On opening /q/:token, the page prompts
for the code before showing quote details or enabling accept. Verification
happens server-side inside the existing security definer functions — the
anon role still gets zero direct table access.
Open questions.
- Code delivery: same share-sheet message as the link, or a second channel (SMS/email) for real two-factor value?
- Code lifetime and retry limits (rate-limiting brute force on a short code).
- Does
accept_quote_by_tokentake the code as a second argument, or does a successful check mint a short-lived session for the page? - Migration impact: new column(s) on
quotes— needs a new migration, init is already pushed territory.
RM-2 — Vectorised logos
Section titled “RM-2 — Vectorised logos”Status: shipped · Area: app, web · Added: 12 June 2026 ·
Shipped: 12 June 2026 (#38)
Problem. The Sawdust logo only exists as raster PNGs
(projects/app/assets/logo.png at 746×266, plus icon.png /
adaptive-icon.png), and /web has no logo asset at all. PNGs blur on
high-DPI screens and in PDFs, can’t be recoloured for dark mode, and each new
size means re-exporting. Quote PDFs and the client-facing /q/:token page
will both need crisp branding.
Shape. Produce the logo and icon as SVG source-of-truth files (shared
location, e.g. a brand/ directory or @sawdust/schemas-style shared
package). Render natively as SVG on /web and the doc-site; export the PNG
sizes Expo needs (icon.png, adaptive-icon.png, splash) from the SVG so
raster assets become build artefacts, not sources.
Open questions.
- Trace the existing PNG or redraw from the original design source (if one exists)?
- SVG in the Expo app:
react-native-svgvs keeping PNG there and only using SVG on web/PDF surfaces. - Where does the canonical asset live so
app,web, and future PDF Lambda all pull from one place?
RM-3 — Invoice chasing with chase history
Section titled “RM-3 — Invoice chasing with chase history”Status: proposed · Area: app, db · Added: 12 June 2026
Problem. When an invoice goes overdue, chasing the client is a manual,
untracked job. The invoice detail screen (app/(app)/invoice/[id].tsx)
already shows an overdue badge, but there’s nothing to act on it — and no
record of whether, when, or how the client was last chased.
Shape. On an overdue invoice, the detail screen shows a Chase button.
Tapping it sends (or opens a prefilled draft of) a chase message to the
client — WhatsApp first, likely via share sheet / whatsapp:// deep link in
the MVP rather than the WhatsApp Business API. Each chase is recorded as an
event, and once one exists the invoice screen shows a chase history list:
what was sent (message description), how it was sent (channel), and the date.
Needs a chase_events table (or similar) keyed to the invoice — new
migration when the feature lands, per migration rules. Fits the existing
chase-messages step in the MVP build order
(“PDF generation, receipt capture, chase messages — in that order”).
Open questions.
- Deep link / share sheet can’t confirm the message was actually sent — log the event on tap, or ask the user to confirm after returning to the app?
- Message content: fixed template, editable before send, or per-trade templates later?
- Channel roadmap: WhatsApp first, then SMS/email? Does the existing
remindersconcept (parked table) merge with chase events or stay separate? - Auto-chase later (scheduled Lambda chasers are already on the API list) — manual chase should write the same event shape so history is unified.
RM-4 — Receipt capture with data extraction
Section titled “RM-4 — Receipt capture with data extraction”Status: proposed · Area: app, db, api · Added: 12 June 2026
Problem. Tradespeople accumulate paper receipts for materials and want them recorded against the job they bought them for. Today there’s no way to capture a receipt in the app, no extraction of supplier/amount/date/VAT, and no link from a receipt to an invoice.
Shape. In the app, take a photo of a receipt (or pick from the photo library); upload the image to Supabase storage. A backend function picks up the upload and extracts the relevant fields — supplier, date, total, VAT — using a vision model or OCR. The architecture doc already earmarks receipts for Claude vision (Haiku 4.5). The user reviews/corrects the extracted fields, then attaches the receipt to an invoice.
Needs receipt image storage (owner-scoped bucket policy), a receipts/
expenses table (currently parked — new migration when this lands), and the
invoice attachment relation.
Open questions.
- Compute home: the locked stack puts backend compute on AWS Lambda (Python), and receipt extraction is already on the Lambda list — a Supabase edge function would be a stack change that needs reopening. Trigger via storage webhook → Lambda?
- Extraction approach: Claude vision (per architecture doc) vs plain OCR — vision handles crumpled/handwritten receipts better; OCR has no per-call model cost. Cost model in the architecture doc should settle this.
- Attach to invoice directly, or to an
expensesrow that’s linked to a job/invoice? (Expenses are also the tax-tracking hook — retention story.) - Sync vs async UX: extraction takes seconds — block with a spinner or notify when parsed?
- Image retention: keep originals for HMRC evidence (likely yes — affects storage cost line).
RM-5 — Suggested line items from past quotes
Section titled “RM-5 — Suggested line items from past quotes”Status: proposed · Area: app, db · Added: 12 June 2026
Problem. Tradespeople quote the same kinds of job repeatedly (“hang a
door”, “skim a ceiling”), and re-type the same line items with the same
prices every time. The history is already sitting in their own quote_items
rows — it just isn’t surfaced while drafting.
Shape. While writing a quote, match the job description being typed against the tradesperson’s previous quotes. When a past job matches, surface its line items as quick adds — one tap inserts the line item (description
- price) into the draft, editable afterwards. Suggestions come only from the user’s own history (owner RLS as usual), never from other accounts.
Related to the parked rate_cards idea — quick adds from history are
effectively an implicit rate card learned from past quotes, and could be the
stepping stone to (or replacement for) explicit rate cards.
Open questions.
- Matching approach: Postgres full-text /
pg_trgmsimilarity on quote descriptions is likely enough to start; embeddings (pgvector) only if fuzzy matching proves too weak. Where does matching run — client-side over the user’s quotes, or an RPC? - Suggest whole past quotes (“repeat of: Bathroom refit, March”) vs individual line items ranked by frequency?
- Cold start: feature is invisible until a few quotes exist — fine, or seed from a trade-specific template pack?
- When prices drift (materials inflation), suggest the most recent price or flag that it’s from an old quote?
RM-6 — Finance page (profit/loss, tax + NI estimate, CIS)
Section titled “RM-6 — Finance page (profit/loss, tax + NI estimate, CIS)”Status: proposed · Area: app, db · Added: 12 June 2026
Problem. The strategy is “getting paid is the acquisition hook; tax is the retention hook” — but there’s currently nowhere in the app that answers “how is my business doing and what will I owe?”. Sole traders juggle this in their heads or a spreadsheet until the January panic.
Shape. A finance page in the app showing, for the current UK tax year (from 6 April):
- Profit / loss — income from paid invoices minus expenses (depends on expenses landing, see RM-4).
- Earnings since April — money actually received this tax year.
- Estimated tax + NI — income tax plus Class 4 (and Class 2 where applicable) National Insurance on profit to date, using current-year thresholds.
- CIS already deducted — tax withheld at source on subcontracted work, offset against the estimated bill.
All money integer pence, formatted at the edge, per schema conventions.
Derivable largely from existing invoices/payments data plus expenses;
CIS needs to be recorded per payment (was this payment CIS-deducted, at what
rate?) — schema addition when this lands.
Hosts the tax set-aside tracker (RM-7).
Open questions.
- Where do tax/NI thresholds live — hardcoded per tax year, config table, or fetched? They change every April and must not silently go stale.
- CIS capture UX: flag on the invoice, on the payment, or per-client (“contractor X always deducts 20%”)?
- Cash basis vs accrual — cash basis is the sole-trader default and matches “earnings since April”; confirm and document the assumption.
- Estimate disclaimers: this is guidance, not advice — wording matters before MTD filing (Phase 3) makes it official.
RM-7 — Tax set-aside tracking
Section titled “RM-7 — Tax set-aside tracking”Status: proposed · Area: app, db · Added: 12 June 2026
Problem. Knowing the estimated tax bill (RM-6) doesn’t mean the money is there in January. Tradespeople want to put money aside as they earn and see whether they’re on track.
Shape. Within the finance page, the user records amounts they’ve set aside for tax (manual entries — Sawdust never holds money). The page shows set aside to date vs estimated bill to date: e.g. “£3,200 of £4,100 covered (78%)” for the current financial year. Optionally suggest a set-aside amount when an invoice is paid (“put away £180 of this”).
Needs a small tax_set_asides table (amount in pence, date, optional note)
— new migration when the feature lands. Coverage % is set-aside total over
RM-6’s estimate, so this depends on RM-6.
Open questions.
- Manual entries only, or also a standing rule (“treat 25% of every payment as set aside”) that auto-records?
- Track against estimated bill to date vs projected full-year bill — which is less misleading mid-year?
- CIS interaction: deductions at source already “cover” part of the bill — count them in the covered figure (probably yes, RM-6 surfaces them)?
- Withdrawals: allow negative entries when the user dips into the pot?
RM-8 — Passwordless OTP email auth (Resend)
Section titled “RM-8 — Passwordless OTP email auth (Resend)”Status: proposed · Area: app, db · Added: 12 June 2026
Problem. Sign-in today uses Supabase email + password with no email verification step. Nothing proves the address belongs to the user, password reset flow doesn’t exist yet, and tradespeople on phones don’t want another password to manage. The longer screens and flows build on the current auth shape, the more painful swapping it gets.
Shape. Replace password auth with passwordless email OTP. Sign-up and
sign-in collapse into one flow: enter email → receive 6-digit code → enter
code → session. Uses supabase.auth.signInWithOtp({ email }) followed by
verifyOtp({ email, token, type: 'email' }). Profile row continues to be
created by the existing signup trigger on auth.users insert — verify
unchanged behaviour against the OTP path.
Email delivery via Resend wired into Supabase as the SMTP provider (dashboard config, no app code change). Supabase’s built-in sender caps at 3/hr and is unusable for production; Resend free tier (3k/mo, 100/day) covers MVP, has DKIM/SPF/DMARC wizard for deliverability, and a Frankfurt EU region that matches the London Supabase project for GDPR.
Open questions.
- Sender domain: which Sawdust-owned domain hosts auth mail, and who owns the DNS records to add Resend’s SPF/DKIM/DMARC entries?
- Email template customisation in Supabase: tradesperson-friendly copy for the OTP message (subject + body), not the default “Confirm your signup”.
- OTP entry UX in the app: single autofocus input with paste support, or six segmented boxes? iOS autofill from Messages works on either.
- Rate limiting: Supabase’s defaults on OTP requests per email — do they hold up against a casual abuser, or do we need an app-side cool-down?
- Resend → SES migration trigger: roughly what monthly volume makes the switch worth it (deliverability parity + template DX both lose)?
- Existing seeded user (
rhys@example.com/password123): rewrite seed to skip the password and rely on the OTP path, or keep password auth as a dev-only escape hatch?
RM-9 — Onboarding data capture + abuse mitigations
Section titled “RM-9 — Onboarding data capture + abuse mitigations”Status: proposed · Area: app, db · Added: 12 June 2026
Problem. Email + OTP (RM-8) proves the address works, nothing more. A bad actor can sign up in seconds and start consuming support bandwidth or filling the DB with junk. At the same time we currently collect zero structured profile data — no trade, no business postcode, no trader address — so quote PDFs, future invoice issuance, and any region-based routing have nothing to draw on. We need enough data to run the product without turning signup into a form wall, and enough friction on abuse-prone actions without punishing real early users.
Shape.
Data capture, laziest-possible cadence:
- Signup: email + OTP only. No extra fields.
- Post-OTP onboarding (before first app screen): display name, trade, business postcode. Postcode (or just outward code) is enough for region context and future lead routing without collecting full address upfront.
- First invoice issued: prompt for full trader address (HMRC requires
it on invoices) and persist on
profiles. Block invoice send until set. - Never collected: DOB, title, full name beyond display name, UTR/NI number (UTR/NI defer to Phase 3 MTD work). GDPR data-minimisation default: if no current feature uses it, don’t ask.
Identity verification: don’t build it. Real identity proof rides on later integrations — Stripe Connect onboarding (when payments land) does KYC and returns a verified business name + address for free; HMRC MTD enrolment (Phase 3) proves trader status. Until then, treat all accounts as unverified and gate on behaviour, not identity.
Abuse mitigations on support / write-heavy actions:
- Per-account rate limits: N support tickets per rolling 7 days, similar caps on quote-create burst rate. Cheap to add as a Postgres count check inside the write RPC.
- Account-age gate: support ticket form disabled for the first 24–48h after signup.
- Disposable email block: reject known throwaway domains at OTP
request time (maintained list, e.g.
disposable-email-domains). - Trust signal on triage (not a hard gate): tickets from accounts with sent quotes / paid invoices surface first; cold accounts queue behind. Avoids punishing a legit user who hits a real bug before their first quote.
- Explicitly rejected: “must have a paid invoice to file a ticket” — locks out exactly the users most likely to need help (new ones hitting bugs in the core flow).
Schema impact: extra columns on profiles (display name, trade,
postcode, address fields, onboarded_at). New support_tickets table
when the support flow itself lands; rate-limit counts live there.
Open questions.
- Trade taxonomy: free-text vs fixed enum (plumber, sparky, chippie, decorator, …)? Enum helps routing/templates later but needs a migration every time we expand. Free-text + an “other” bucket probably fine for MVP.
- Postcode validation: format check only, or hit a postcode API (Postcodes.io is free) to confirm it resolves? Latter catches typos and gives us coordinates for routing for free.
- Where do onboarding fields live — extend
profiles, or a separatetrader_detailstable?profilesis simpler; split only if a multi-trader-per-account model ever appears (unlikely for sole-trader scope). - Address capture UX: full UK address lookup (Loqate / GetAddress / OS Places — paid) vs manual entry? Manual is free and fine until volume justifies a lookup contract.
- Ticket rate-limit numbers: what’s the right N? Pick something conservative (3/week) and revisit when real abuse data exists.
- Disposable email list maintenance: pull at build time vs runtime fetch; risk of false positives (some legit users use Fastmail aliases that look disposable).
- Interaction with RM-1: client-side verification protects the recipient; this item protects the platform. Separate concerns, no shared schema.
RM-10 — Legal + compliance foundation
Section titled “RM-10 — Legal + compliance foundation”Status: proposed · Area: business (non-code) · Added: 12 June 2026
Problem. Sawdust handles trader PII, their clients’ PII, payment data (via Stripe), and — once RM-6 lands — tax estimates that users will act on. None of the legal scaffolding that lets a UK SaaS do that exists yet: no Ltd entity, no ICO registration, no privacy policy, no Terms of Service, no sub-processor list, no PI insurance. Shipping without it is a regulatory and personal-liability problem, not a “tidy up later” one.
Shape. Treat as a parallel work stream to the product roadmap rather than a feature. Items aren’t code; they live in the business, but they gate when product features can ship.
Entity + registrations:
- Limited company at Companies House before public launch. Personal liability shield + contracting capacity with Stripe / Resend / AWS as a business. Domain, bank account, and supplier accounts move to the Ltd.
- ICO data protection fee (£40–60/yr) — mandatory for any business processing personal data. Public register.
- Sole-trader-as-Sawdust acceptable pre-launch only.
Data protection (UK GDPR / DPA 2018):
- Privacy policy — what’s collected, lawful basis (contract for
traders, legitimate interest for their clients on
/q/:token), retention, subject rights, contact for DSARs. - Cookie / tracker notice — only if non-essential cookies appear on
/webor doc-site. Current default is none — document the “no non-essential cookies” position rather than ship a banner. - DSAR runbook — how access / deletion requests get handled. Inbox + internal checklist fine for MVP.
- DPIA (light) — produced when receipt OCR (RM-4) and tax data land. Receipts may incidentally contain special-category data; vision model usage needs documenting.
- Sub-processor list kept in the repo, with the DPA signed/accepted for each: Supabase, AWS, Resend, Anthropic, Stripe, Expo.
- Article 30 records of processing — short internal doc, required even at this size.
Trader / client contracts:
- Terms of Service (trader-facing): liability cap, no warranty on tax estimates, acceptable use, account termination, jurisdiction England & Wales.
/q/:tokennotice: short “Sawdust is acting on behalf of [trader]. Privacy: …” so the client knows who controls their data.- Controller / processor split: ToS states traders are controllers of their own clients’ data; Sawdust is processor for that. Standard SaaS pattern; needs DPA-style language baked into the ToS.
Payments:
- Stripe ToS acceptance shifts most acquirer / PCI burden onto Stripe. Staying on Payment Links keeps us at PCI SAQ-A (lowest tier) because we never touch card data.
- Refunds + cancellation rights wording in ToS, including UK CCRs 14-day cooling-off for digital services (and the standard waiver).
Sawdust’s own tax:
- VAT registration — only at the £90k threshold, but the trigger must be watched; late registration is fined.
- MTD for ITSA software recognition — separate HMRC process with real lead time. Required before Phase 3 filing features can ship. Track now, action later.
Brand:
- IPO trade mark search for “Sawdust” in classes 9 + 42 before more design spend (intersects with RM-2). ~£170 to register if clear.
- Domain ownership moved to the Ltd, not personal email.
Insurance:
- Professional indemnity — covers “tax estimate was wrong, user got fined”. Required before RM-6 ships estimates publicly.
- Cyber liability — breach response cost. Optional pre-revenue, sensible once user count grows.
Co-founder:
- Founders’ agreement / shareholders’ agreement before any equity split is committed anywhere. Cheap now, expensive later.
Priority order (gates real product milestones).
- Ltd company + ICO registration — before any public sign-up.
- Privacy policy + ToS drafted — before public sign-up.
- Sub-processor list + DPAs collected — rolling, before launch.
- PI insurance — before RM-6 tax estimates go live.
- Trade mark search — before RM-2 escalates brand spend.
- Founders’ agreement — before external money, hires, or equity issue.
- MTD software recognition — start the process when Phase 2 ends, well before Phase 3 ships.
Open questions.
- Solicitor for ToS + privacy policy, or template (SeedLegals / Genie AI / Rocket Lawyer) reviewed by a solicitor? Templates are cheap but tax-software liability wording is non-standard.
- Where do the legal docs live —
projects/doc-siteso they’re public and versioned, or hosted via a CMS so non-engineers can update? Likely doc-site for MVP, revisit if a legal team appears. - Insurance broker route vs direct (Hiscox / Markel / PolicyBee) for PI — PolicyBee specialises in tech, often cheapest at this size.
- Companies House SIC code: 62012 (business and domestic software development) is the obvious fit; check whether 62020 (computer consultancy) needs adding for invoicing flexibility.
- VAT scheme choice once threshold hit — Flat Rate vs standard. Flat Rate is simpler but worse if input VAT is high (it isn’t for pure SaaS).