Example app spec: a freelance invoice tracker

THE WHOLE IDEA WE TYPED

“An invoice tracker for freelancers that reminds clients when payments are late”

Generated by SpecWise with the Balanced model (3 credits) · 2,968 words · unedited

Want one for your idea?

Describe your idea in a sentence and get a spec like this in about a minute.

Generate my spec

7-day free trial · no card needed

Invoice Nudge - Product Requirements Document

1) Summary

Invoice Nudge is a web app for solo freelancers who track the invoices they send and want late payers chased automatically. The freelancer logs an invoice (client, amount, due date); each day the app computes its status (Draft, Sent, Overdue, Paid) and, once an invoice is overdue, emails the client a polite reminder on a fixed schedule (3, 10 and 21 days after the due date) until it is paid. Data lives in a Supabase Postgres database behind email-magic-link login; reminders are sent by a daily scheduled job.

  • Assumption: Storage and ownership: server-backed because reminders must reach clients while the freelancer's browser is closed. Each freelancer sees only their own rows (row-level security on user_id).
  • Assumption: "Late" means the local calendar date in the freelancer's time zone is after due_date and the invoice is not Paid, not "more than 24 hours past a timestamp".
  • Assumption: Day boundary: dates are plain calendar dates (no time of day), computed in the freelancer's timezone setting (default UTC, captured from the browser at first sign-in).
  • Assumption: Caps: client name 60 characters, invoice number 20, note 200, amount 0.01 to 999,999.99, at most 500 invoices and 100 clients per account.
  • Assumption: Reminder rule: exactly one reminder per invoice per step at 3, 10 and 21 days past due (see 4); after step 3 the app stops emailing and only flags the invoice Overdue in the freelancer's list.
  • Assumption: A single currency per account (USD, EUR or GBP, chosen at first sign-in); no conversion.
  • Assumption: Platform: responsive browser app for phone and desktop; not installable and no push notifications, since all reminders go to the client by email.
  • Assumption: Nothing is automatic beyond reminders: the app never marks an invoice Paid itself, never emails the freelancer, and has no undo for a sent reminder.

2) Tech Stack

  • Framework → Next.js 14 (App Router) → server components and route handlers in one project
  • Language → TypeScript → typed data model
  • Database → Supabase Postgres → relational data plus row-level security
  • Auth → Supabase Auth, email magic link → no passwords to store
  • Email → Resend (REST API) → transactional reminder emails
  • Scheduler → Supabase pg_cron calling a Supabase Edge Function once per day at 08:00 UTC → Vercel Hobby cron cannot run more often than daily and keeping it in Supabase keeps secrets in one place
  • Styling → Tailwind CSS → fast responsive UI
  • Dates → date-fns → formatting and day differences
  • Deployment → Vercel (web app) + Supabase (database and function)

3) Users & Screens

RoleCanCannot
Freelancer (signed in)Create, edit, delete own clients and invoices; mark paid; send a reminder nowSee other users' data
Client (email recipient only)Receive reminder emailsSign in or see the app

Screens:

ScreenReached fromShowsEmpty / loading / errorBack / close goes to
Sign inAny URL when signed outEmail field, Send magic link buttonSent: Check your email for the sign-in link. Error: Could not send the link. Try again.n/a
Invoices (home)After sign-in; header linkTotals strip, filter tabs, invoice listEmpty: No invoices yet. Add your first one. with New invoice button. Loading: 3 grey skeleton rows. Error: Could not load invoices. with Retryn/a
Invoice form (sheet)New invoice or row EditFields, Save, Cancelsee 6.1Invoices
Invoice detailTap an invoice rowFields, status, reminder history, actionsReminder history empty: No reminders sent yet.Invoices
ClientsHeader link ClientsClient list with Edit/DeleteEmpty: No clients yet. Add one when you create an invoice.Invoices

Navigation: header with the app name, links Invoices and Clients, and Sign out.

4) Data Model & Logic

Profile: user_id (uuid, PK = auth user), timezone (text, e.g. Europe/Berlin, default UTC), currency (enum USD|EUR|GBP, default USD), sender_name (text, ≤ 60, shown in emails, default = the part of the email before @).

Client: id (uuid), user_id, name (≤ 60, required), email (valid format, required). Unique on (user_id, lower(email)).

Invoice: id, user_id, client_id, number (≤ 20, required, unique per user_id), amount (numeric 2 dp, 0.01 to 999,999.99), issue_date (date), due_date (date, on or after issue_date), note (≤ 200, optional), status (enum Draft|Sent|Paid, default Draft), paid_on (date, null until paid), reminders_enabled (bool, default true).

Reminder: id, invoice_id, step (1, 2 or 3), sent_at (timestamp), result (sent|failed). Unique on (invoice_id, step).

Sort: Invoices list sorts by due_date ascending, with Overdue rows first. Retention: records are kept until the freelancer deletes them.

Logic:

today = current date in profile.timezone
daysBetween(a, b) = round((Date.UTC(b.y,b.m,b.d) - Date.UTC(a.y,a.m,a.d)) / 86400000)

displayStatus(inv):
  if inv.status == Paid:  "Paid"
  if inv.status == Draft: "Draft"
  if today > inv.due_date: "Overdue"
  else: "Sent"

daysOverdue(inv) = daysBetween(inv.due_date, today)      # only when Overdue
stepDue(d) = d >= 21 ? 3 : d >= 10 ? 2 : d >= 3 ? 1 : 0

dailyJob (08:00 UTC): for each Sent invoice with reminders_enabled
  s = stepDue(daysOverdue)
  if s > 0 and no Reminder row exists for (invoice, s): send email, insert Reminder
  # If several steps were missed (job outage), send only step s, never the earlier ones.

Worked example (account timezone Europe/Berlin, today = 2025-03-20):

Invoicedue_datestatusdaysOverdueDisplayJob action
#1012025-03-18Sent2OverduestepDue = 0, nothing
#1022025-03-10Sent10OverduestepDue = 2; step 2 sent if missing
#1032025-02-27Sent21OverduestepDue = 3; step 3 sent if missing
#1042025-03-20Sent0Sent (due today is not late)nothing

Midnight crossing: at 23:30 on 2025-03-20 Berlin time, #104 is still Sent; at 00:05 on 2025-03-21 it becomes Overdue with daysOverdue 1 (no email until day 3, 2025-03-24).

Status transitions:

FromEventToSide effect
DraftMark as sentSentnone (the original invoice is sent by the freelancer outside the app)
SentMark as paidPaidset paid_on = today; reminders stop
PaidReopenSentclear paid_on
SentMove to draftDraftreminders stop

All three statuses can be exited; there is no terminal status. Overdue is a display state derived from Sent, never stored.

Edits and edge cases: changing due_date to a later date deletes existing Reminder rows whose step is no longer due, so reminders can fire again; changing it earlier does not resend steps already sent. Deleting a client with invoices is blocked. Amount of zero or negative is rejected. A manual Send reminder now sends the next unsent step (or step 3 again if all sent) and logs it as a Reminder row.

5) External Services

Supabase

Used for: database, auth, pg_cron schedule, Edge Function. Calls: supabase.auth.signInWithOtp, table select/insert/update/delete, pg_cron job daily-reminders at 0 8 * * *. Fields read: all tables in 4. Limits: free tier 500 MB database, 2 emails/hour for auth unless a custom SMTP is set (Resend SMTP is configured for auth emails). Failure: Could not load invoices. with Retry. Keys: NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY (browser); service-role key (server and Edge Function only).

Resend

Used for: sending reminder emails. Calls: POST https://api.resend.com/emails with from, to, subject, text. Fields read: client name, email; invoice number, amount, due_date; profile sender_name. Limits: 100 emails/day on the free plan; the job stops at 100 and resumes next run. Failure: reminder saved with result = failed, detail view shows Reminder failed to send. with a Retry button; the job retries failed steps on its next run. Keys: RESEND_API_KEY (server and Edge Function only).

6) Core Features

6.1 Create and Edit Invoice

User Flow:

  1. On Invoices, tap New invoice; the form sheet opens with heading New invoice.
  2. Choose a client from the Client dropdown or tap Add new client (opens inline Name and Email fields).
  3. Fill Invoice number, Amount, Issue date (default today), Due date (default today + 14 days), optional Note.
  4. Tap Save invoice; it is stored as Draft and the sheet closes.
  5. Row Edit reopens the same form with heading Edit invoice; Delete asks Delete invoice #{number}? This cannot be undone. with Delete and Cancel.

UI Copy & Colors:

  • Primary button: Save invoice, white on #2563EB
  • Delete confirm button: Delete, white on #DC2626
  • Heading text #0F172A on #F8FAFC

Error & Empty States:

  • Empty required field → This field is required. Recovery: fill it.
  • Due before issue → Due date must be on or after the issue date. Recovery: change a date.
  • Duplicate number → You already have an invoice with this number. Recovery: change the number.
  • Invalid email → Enter a valid email address.

Acceptance Checks:

  • ✓ Number 101, amount 1250.5, due 2025-03-10 → saved, row shows #101, 1,250.50, status Draft.
  • ✓ Due 2025-03-01, issue 2025-03-05 → error Due date must be on or after the issue date.; nothing saved.
  • ✓ Amount 0 → Amount must be between 0.01 and 999,999.99.
  • ✓ Deleting a client that has invoices → This client has invoices. Delete or reassign them first.

6.2 Invoice List and Status

User Flow:

  1. Invoices shows a totals strip: Outstanding (sum of Sent + Overdue), Overdue (sum of overdue), Paid this month.
  2. Filter tabs: All, Draft, Sent, Overdue, Paid; default All.
  3. Each row shows number, client name, amount, due date (d MMM yyyy), and a status pill.
  4. Overdue rows add {n} days overdue.

UI Copy & Colors:

  • Pills: Draft #64748B, Sent #2563EB, Overdue #DC2626, Paid #16A34A, white text.
  • Overdue count text: 3 days overdue; singular 1 day overdue.

Error & Empty States:

  • Filter with no matches → No {status} invoices.
  • Load failure → Could not load invoices. Recovery: Retry.

Acceptance Checks:

  • ✓ Today 2025-03-20, invoice due 2025-03-18, Sent → pill Overdue, text 2 days overdue.
  • ✓ Invoice due 2025-03-20, Sent → pill Sent.
  • ✓ #101 1,000.00 Sent-overdue and #102 500.00 Sent-not-yet-due → Outstanding 1,500.00, Overdue 1,000.00.
  • ✓ Tab Paid with none → No Paid invoices.

6.3 Mark Sent and Mark Paid

User Flow:

  1. On a Draft detail screen tap Mark as sent; status becomes Sent.
  2. On a Sent or overdue invoice tap Mark as paid; a confirm sheet asks Mark invoice #{number} as paid on {date}? with a date field defaulting to today and buttons Confirm and Cancel.
  3. On a Paid invoice tap Reopen; status returns to Sent and paid_on clears.

UI Copy & Colors:

  • Mark as paid: white on #16A34A.
  • Reopen: #0F172A on #F8FAFC, 1 px border #64748B.

Error & Empty States:

  • Paid date before issue date → Paid date cannot be before the issue date.
  • Save failure → Could not update the invoice. Recovery: tap the button again.

Acceptance Checks:

  • ✓ Overdue invoice, Mark as paid with 2025-03-21 → status Paid, paid_on 2025-03-21, no further reminders.
  • ✓ Reopen a Paid invoice with due_date 2025-03-10 on 2025-03-20 → status shows Overdue, paid_on empty.
  • ✓ Mark as sent on Draft → pill Sent; no email goes to the client.

6.4 Automatic Late Reminders

User Flow:

  1. Each day at 08:00 UTC the job evaluates every Sent invoice with reminders_enabled per 4.
  2. When a step is due and not yet sent, the app emails the client and logs a Reminder row.
  3. The freelancer sees the log under Reminders on the invoice detail screen: Step {n} sent {d MMM yyyy}.
  4. A toggle Send automatic reminders on the detail screen sets reminders_enabled.

Email text (plain text):

  • Step 1 subject: Friendly reminder: invoice #{number} is past due
  • Step 2 subject: Second reminder: invoice #{number} is 10 days past due
  • Step 3 subject: Final reminder: invoice #{number} is 21 days past due
  • Body: Hi {client name}, invoice #{number} for {amount} {currency} was due on {d MMM yyyy} and is still unpaid. Please arrange payment or reply to this email if there is a problem. Thank you, {sender_name}
  • Reply-To is the freelancer's sign-in email, so client replies reach them.

Error & Empty States:

  • Send failure → reminder row result = failed; detail shows Reminder failed to send. Recovery: Retry, or the next daily run retries.
  • Reminders toggled off → Automatic reminders are off for this invoice.

Acceptance Checks:

  • ✓ Due 2025-03-10, today 2025-03-13 (3 days) → one email with subject Friendly reminder: invoice #101 is past due; Step 1 sent 13 Mar 2025.
  • ✓ The job runs twice on 2025-03-13 → still exactly one step-1 email.
  • ✓ Job outage until day 11 → sends the step 2 email only, not step 1.
  • ✓ Toggle off → no email at any step; toggle on at day 12 → step 2 email on the next run.

6.5 Send Reminder Now

User Flow:

  1. On an overdue invoice detail screen, tap Send reminder now.
  2. A confirm sheet shows the client email and asks Send a reminder to {email}? with Send and Cancel.
  3. On Send, the next unsent step is emailed immediately and logged; the button is disabled for 24 hours after any send with the label Sent today.

UI Copy & Colors:

  • Send reminder now: white on #F59E0B, text #0F172A.
  • Success toast: Reminder sent to {email}. on #16A34A.

Error & Empty States:

  • Invoice not overdue → button hidden.
  • Send failure → toast Could not send the reminder. Try again. on #DC2626.

Acceptance Checks:

  • ✓ Overdue invoice, steps 1-2 sent → tap sends the step 3 text and logs Step 3.
  • ✓ All three steps sent → tap re-sends step 3 and adds a logged row.
  • ✓ After sending, the button reads Sent today and is disabled.

7) UI/UX

Palette:

TokenHexUsed for
primary#2563EBbuttons, Sent pill
success#16A34APaid, success toast
warning#F59E0BSend reminder now
danger#DC2626Overdue, delete
text#0F172Abody text
muted#64748BDraft, captions
surface#F8FAFCpage background

Typography: Inter, 16 px base; headings 24 px bold; totals numbers 28 px bold; captions 13 px.

Layout: single column, mobile first, max width 480 px centred on phones; at 1280 px the list and detail use a centred 720 px column. Totals strip sits at the top of Invoices.

Motion:

InteractionDurationEasing
Sheet open/close200 msease-out
Toast in/out150 msease-in-out
Button press100 msease-out

Timestamp display: dates shown as d MMM yyyy (date-fns format d MMM yyyy); reminder log shows Step {n} sent d MMM yyyy, converted to the profile timezone.

Accessibility: text contrast ≥ 4.5:1, tap targets ≥ 44×44 px, visible focus ring 2 px #2563EB, aria-label on icon buttons.

8) Infrastructure Notes

Environment variables: Browser: NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY. Server and Edge Function only: SUPABASE_SERVICE_ROLE_KEY, RESEND_API_KEY, RESEND_FROM_ADDRESS.

Client vs server: the browser reads and writes invoices and clients through Supabase with row-level security. Emails are sent only from the Edge Function and a Next.js route handler for Send reminder now; the Resend key never reaches the browser.

Platform limits:

LimitPlatforms affectedDesign answer
Vercel Hobby cron runs once per day maximumVercelUse Supabase pg_cron instead
Email spam filteringAll clientsVerified sending domain, plain-text body, Reply-To set
Resend free plan 100 emails/dayResendJob caps at 100 and resumes next run

9) Out of Scope (V1)

  1. PDF invoice generation - tempting because freelancers need one, but the pitch tracks invoices, not creates them.
  2. Online payment links (Stripe) - tempting to let clients pay from the email, but it adds payment compliance.
  3. Multi-currency accounts - one currency per account keeps totals simple.
  4. Customisable reminder schedule and templates - the fixed 3/10/21-day plan covers the pitch.
  5. Recurring invoices - a likely next ask, but a second system.
  6. Client portal or login - clients only receive email.
  7. Team accounts and shared access - the app is for one freelancer.

10) Success Criteria

  • ✓ A magic-link email arrives and signs the user in within 30 s.
  • ✓ Creating invoice #101 for 1,250.50 with a due date saves it as Draft with the correct row.
  • ✓ A Sent invoice due yesterday shows Overdue and 1 day overdue.
  • ✓ An invoice due today still shows Sent.
  • ✓ Three days past due, the job sends exactly one step-1 email with the subject in 6.4.
  • ✓ Running the job twice the same day never sends a duplicate.
  • ✓ Mark as paid stops all further reminders.
  • ✓ Send reminder now emails the client and disables itself for 24 hours.
  • ✓ A second user cannot see the first user's invoices.
  • ✓ No console errors during create invoice, mark sent, mark paid and send reminder.

Spec your own app

Describe your idea in a sentence and get a spec like this in about a minute.

Generate my spec

7-day free trial · no card needed

More examples