All work

Rules-driven invoicing platform with operator and client portals

Auto-Invoice

A full-stack TypeScript platform that ingests usage spreadsheets from Google Drive, runs them through a declarative pricing-rules engine, and turns the result into invoices, ledger entries and versioned PDFs, with separate operator and client portals behind role-based access.

Context
Professional · National Sales Partners
Role
Primary developer
Period
2025 — 2026
Stack
TypeScript, Next.js 16, React 19, Supabase (Postgres · Auth · Storage), Google OAuth

At a glance

  1. 01 · Problem

    Turning daily usage spreadsheets into invoices under different rules for every client is repetitive, and an invoice that is wrong, duplicated or missing a day is expensive to unwind.

  2. 02 · Solution

    A Next.js and Supabase platform that ingests spreadsheets from Google Drive, prices them with declarative per-client rules, and produces invoices, ledger entries and versioned PDFs.

  3. 03 · Impact

    One repeatable flow from raw spreadsheet to finished invoice. Imports and invoice creation are safe to re-run, rule changes are template edits rather than code changes, and clients only ever see their own documents.

Framework
Next.js 16 App Router · React 19
Language
TypeScript end to end
Data
Supabase Postgres · Storage buckets
Auth
Supabase Auth · SSR cookie sessions
Ingest
Google OAuth 2.0 · Drive API · XLSX
Engine
Declarative JSON pricing rules
Documents
pdf-lib rendering · versioned storage
Access
Operator / client roles · route + API guards

The problem

Invoicing from usage data starts with a folder of daily spreadsheets and ends with a document a client will check line by line. Every client is billed under different rules, spreadsheets arrive late or get re-exported, and an invoice that is wrong, duplicated or missing a day is expensive to unwind. The job was to make the whole path from raw file to finished invoice repeatable, auditable and safe to re-run.

Architecture

Users & sourcesNext.js 16 · App RouterSupabaseOperator portaltemplates · teamClient portalclient role onlyGoogle DriveOAuth 2.0 · daily XLSXproxyrefreshes session cookies on each requestROUTE HANDLERSPOST /api/google/ingestDrive → rowsPOST /api/invoices/previewdry-run invoicePOST /api/invoices/createsaga + 409 guardPOST /api/paymentstoken · role checkDOMAIN LIBRARY (PURE TYPESCRIPT)Rules enginealias → filter → group → priceMapping cascadeexact → prefix → regexPDF builderpdf-lib · versionedAuthusers · profiles · rolesPostgresstats · invoices · ledgerStorageinvoice PDFs, per versionupsertService-role key used only inside route handlers · browser code never sees it
Architecture, simplified · provider shown genericallyTypeScript · Next.js 16 · React 19 · scroll →

Engineering patterns

Approach

Usage data comes straight from Google Drive. Each user connects Drive with OAuth; an ingest route lists the folder, parses each workbook with SheetJS, normalizes rows and upserts them in chunks keyed on (user, date, campaign, source file), so importing the same file twice changes nothing.

Imports are incremental: a per-user cursor on file modification time means only new or re-exported files are read. The cursor only advances after a run that actually imported something, so an empty or failed run can never skip data (a bug I found and fixed in production: cursor poisoning).

Billing rules are data, not code. Each client has a JSON template the engine evaluates in five stages: header aliases, filters, grouping, metrics (COUNT, COUNT_WHERE, SUM_COLUMN) and pricing blocks whose quantities are tokens such as {{billable_events}}, with minimum floors, rounding and tax.

Creating an invoice previews first with the same engine, refuses a second invoice for the same client and period (HTTP 409), then writes the invoice, its lines and a ledger entry, compensating (deleting) earlier writes if a later one fails. The PDF is rendered with pdf-lib and stored as a new document version.

What was built

  • 01Google Drive connection with OAuth and folder selection
  • 02Incremental and full-resync spreadsheet ingest
  • 03Rules-driven pricing templates per client
  • 04Campaign-to-client mapping: exact, prefix and regex rules
  • 05Live invoice preview before anything is written
  • 06Duplicate-period guard and compensating rollback
  • 07Ledger entries and recorded payments per invoice
  • 08Versioned PDF documents in object storage
  • 09Operator portal: clients, team, templates, generation
  • 10Client portal: their invoices and documents only
TypeScriptNext.js 16React 19Supabase (Postgres · Auth · Storage)Google OAuthDrive APISheetJSpdf-libTailwind CSS

The hard parts

Idempotent by construction

Re-running an import or retrying a request must never double-count. Composite conflict keys on upsert and the duplicate-period guard make every write safe to repeat.

No partial invoices

Postgres writes go through a REST client without one wrapping transaction, so the create flow is a small saga: each step has a compensating delete that runs if a later step fails.

Rules people can change

Pricing differs per client and changes over time. Putting it in a typed JSON DSL with a pure evaluator means new rules are a template edit, and the same function powers preview and final invoice.

Two audiences, one app

Operators and clients share one codebase but get different areas: sessions refresh in a Next.js proxy, pages are guarded by role, and API handlers re-check the caller before touching data.

Outcome

  • From raw spreadsheet to finished invoice PDF in one flow
  • Imports and invoice creation safe to re-run
  • Separate operator and client portals on one codebase