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 afterdue_dateand the invoice is notPaid, 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
timezonesetting (defaultUTC, 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
Overduein the freelancer's list. - Assumption: A single currency per account (
USD,EURorGBP, 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
Paiditself, 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_croncalling 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
| Role | Can | Cannot |
|---|---|---|
| Freelancer (signed in) | Create, edit, delete own clients and invoices; mark paid; send a reminder now | See other users' data |
| Client (email recipient only) | Receive reminder emails | Sign in or see the app |
Screens:
| Screen | Reached from | Shows | Empty / loading / error | Back / close goes to |
|---|---|---|---|---|
| Sign in | Any URL when signed out | Email field, Send magic link button | Sent: Check your email for the sign-in link. Error: Could not send the link. Try again. | n/a |
| Invoices (home) | After sign-in; header link | Totals strip, filter tabs, invoice list | Empty: No invoices yet. Add your first one. with New invoice button. Loading: 3 grey skeleton rows. Error: Could not load invoices. with Retry | n/a |
| Invoice form (sheet) | New invoice or row Edit | Fields, Save, Cancel | see 6.1 | Invoices |
| Invoice detail | Tap an invoice row | Fields, status, reminder history, actions | Reminder history empty: No reminders sent yet. | Invoices |
| Clients | Header link Clients | Client list with Edit/Delete | Empty: 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):
| Invoice | due_date | status | daysOverdue | Display | Job action |
|---|---|---|---|---|---|
| #101 | 2025-03-18 | Sent | 2 | Overdue | stepDue = 0, nothing |
| #102 | 2025-03-10 | Sent | 10 | Overdue | stepDue = 2; step 2 sent if missing |
| #103 | 2025-02-27 | Sent | 21 | Overdue | stepDue = 3; step 3 sent if missing |
| #104 | 2025-03-20 | Sent | 0 | Sent (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:
| From | Event | To | Side effect |
|---|---|---|---|
Draft | Mark as sent | Sent | none (the original invoice is sent by the freelancer outside the app) |
Sent | Mark as paid | Paid | set paid_on = today; reminders stop |
Paid | Reopen | Sent | clear paid_on |
Sent | Move to draft | Draft | reminders 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:
- On Invoices, tap
New invoice; the form sheet opens with headingNew invoice. - Choose a client from the
Clientdropdown or tapAdd new client(opens inlineNameandEmailfields). - Fill
Invoice number,Amount,Issue date(default today),Due date(default today + 14 days), optionalNote. - Tap
Save invoice; it is stored asDraftand the sheet closes. - Row
Editreopens the same form with headingEdit invoice;DeleteasksDelete invoice #{number}? This cannot be undone.withDeleteandCancel.
UI Copy & Colors:
- Primary button:
Save invoice, white on#2563EB - Delete confirm button:
Delete, white on#DC2626 - Heading text
#0F172Aon#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, amount1250.5, due 2025-03-10 → saved, row shows#101,1,250.50, statusDraft. - ✓ 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:
- Invoices shows a totals strip:
Outstanding(sum ofSent+Overdue),Overdue(sum of overdue),Paid this month. - Filter tabs:
All,Draft,Sent,Overdue,Paid; defaultAll. - Each row shows number, client name, amount, due date (
d MMM yyyy), and a status pill. - 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; singular1 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→ pillOverdue, text2 days overdue. - ✓ Invoice due 2025-03-20,
Sent→ pillSent. - ✓ #101
1,000.00Sent-overdue and #102500.00Sent-not-yet-due →Outstanding1,500.00,Overdue1,000.00. - ✓ Tab
Paidwith none →No Paid invoices.
6.3 Mark Sent and Mark Paid
User Flow:
- On a
Draftdetail screen tapMark as sent; status becomesSent. - On a
Sentor overdue invoice tapMark as paid; a confirm sheet asksMark invoice #{number} as paid on {date}?with a date field defaulting to today and buttonsConfirmandCancel. - On a
Paidinvoice tapReopen; status returns toSentandpaid_onclears.
UI Copy & Colors:
Mark as paid: white on#16A34A.Reopen:#0F172Aon#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 paidwith 2025-03-21 → statusPaid,paid_on2025-03-21, no further reminders. - ✓
Reopena Paid invoice withdue_date2025-03-10 on 2025-03-20 → status showsOverdue,paid_onempty. - ✓
Mark as sentonDraft→ pillSent; no email goes to the client.
6.4 Automatic Late Reminders
User Flow:
- Each day at 08:00 UTC the job evaluates every
Sentinvoice withreminders_enabledper 4. - When a step is due and not yet sent, the app emails the client and logs a
Reminderrow. - The freelancer sees the log under
Reminderson the invoice detail screen:Step {n} sent {d MMM yyyy}. - A toggle
Send automatic reminderson the detail screen setsreminders_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-Tois the freelancer's sign-in email, so client replies reach them.
Error & Empty States:
- Send failure → reminder row
result = failed; detail showsReminder 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:
- On an overdue invoice detail screen, tap
Send reminder now. - A confirm sheet shows the client email and asks
Send a reminder to {email}?withSendandCancel. - On
Send, the next unsent step is emailed immediately and logged; the button is disabled for 24 hours after any send with the labelSent 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 todayand is disabled.
7) UI/UX
Palette:
| Token | Hex | Used for |
|---|---|---|
| primary | #2563EB | buttons, Sent pill |
| success | #16A34A | Paid, success toast |
| warning | #F59E0B | Send reminder now |
| danger | #DC2626 | Overdue, delete |
| text | #0F172A | body text |
| muted | #64748B | Draft, captions |
| surface | #F8FAFC | page 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:
| Interaction | Duration | Easing |
|---|---|---|
| Sheet open/close | 200 ms | ease-out |
| Toast in/out | 150 ms | ease-in-out |
| Button press | 100 ms | ease-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:
| Limit | Platforms affected | Design answer |
|---|---|---|
| Vercel Hobby cron runs once per day maximum | Vercel | Use Supabase pg_cron instead |
| Email spam filtering | All clients | Verified sending domain, plain-text body, Reply-To set |
| Resend free plan 100 emails/day | Resend | Job caps at 100 and resumes next run |
9) Out of Scope (V1)
- PDF invoice generation - tempting because freelancers need one, but the pitch tracks invoices, not creates them.
- Online payment links (Stripe) - tempting to let clients pay from the email, but it adds payment compliance.
- Multi-currency accounts - one currency per account keeps totals simple.
- Customisable reminder schedule and templates - the fixed 3/10/21-day plan covers the pitch.
- Recurring invoices - a likely next ask, but a second system.
- Client portal or login - clients only receive email.
- 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.50with a due date saves it asDraftwith the correct row. - ✓ A
Sentinvoice due yesterday showsOverdueand1 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 paidstops all further reminders. - ✓
Send reminder nowemails 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.