Skip to content

Case study, open source

ContractPal

Photograph a confusing letter. Get back what it actually says.

Role
Author
Period
2026
Status
Built; live-API verification pending
License
AGPL-3.0
Latest
Updated Aug 2026

Source on GitHub (opens in a new tab)

Overview

Reads insurance policies, bills, contracts and renewal notices and answers three questions: what is this, what do I need to do, and what’s the fine print that could cost me. Nothing you send is stored, and that is enforced in code, not promised in a policy.

2
screens. That is the whole product.
0
databases, for documents or for customers
9
ADRs, including options deliberately not taken
17px
minimum type size, enforced by a check

Problem

Insurance policies, bills and renewal notices are written for the sender, not the reader.

ContractPal is for people who find paperwork stressful rather than interesting: often older, often on a phone, and not interested in learning an app. They also shouldn’t have to trust one with their documents.

What I built

  1. Upload Hub: one large button. Take a photo or pick a file.
  2. Breakdown: The Big Picture, What You Actually Need to Do, The Hidden Gotchas. Dates and amounts are emphasised, because those are what people scan for.
  3. Next.js 16 as a single deployable. No CSS framework, no component library, no database; tests run on Node’s built-in runner.
  4. One free scan, then a flat subscription through Stripe, with recovery links for subscribers who clear cookies or change device.

Architecture

One Next.js deployable. A document lives only as a buffer inside one request and passes through a provider-agnostic AI seam. There is no database: a signed, expiring token is the entitlement and Stripe holds subscription state.

A document’s whole life: one request
  1. Photo or PDF

    usually from a phone

    The only input. There is no account to create before the first scan.

  2. Upload Hub

    validation, rate limit

    One large button. Uploads are validated and rate-limited, and both are covered by the Node test suite.

  3. /api/analyze

    a buffer inside one request

    The document exists only as an in-memory buffer for the life of the request. It is never written to disk, a database, cloud storage or a log.

  4. Provider seam

    one environment variable

    Vendor adapters behind one interface. Each adapter carries its provider’s retention and jurisdiction facts in code, and the privacy page is written from them.

  5. AI providers

    Anthropic, OpenAI, Mistral, OpenRouter

    Admitted only if they don’t train on API data, publish a retention window, state a jurisdiction and offer zero retention. OpenRouter is restricted to zero-retention endpoints and has no default model.

  6. Breakdown

    three plain sections

    The second and last screen. The Content-Security-Policy sets connect-src ’self’, so the page physically cannot send a document to another origin.

  7. Entitlement token

    signed, expiring

    The signed token is the entitlement. There is no customer table. Recovery tokens use a distinct audience, and the recovery endpoint always answers 202 so it can’t enumerate subscribers.

  8. Stripe

    holds subscription state

    One free scan, then one flat monthly price. Stripe holds subscription state; the client holds a signed token.

  9. audit:retention

    fails the build

    A build-time audit that fails if any code path could persist a document or log its content. It is negative-tested against a deliberately violating file.

Decisions

  1. Zero retention is a build failure, not a promise

    npm run audit:retention fails the build if any code path could write a document anywhere, and it is negative-tested against a file that deliberately violates it.

  2. No database anywhere

    A signed, expiring entitlement token is the entitlement; Stripe holds subscription state.

  3. Privacy criteria gate every AI provider

    The vendor is one environment variable, but a provider is only admitted if it meets published retention and training criteria. With a gateway the model slug decides who reads the document, so OpenRouter has no default. Guessing one would be a silent privacy decision.

  4. The honest part, said plainly

    The AI provider sees the document transiently and may retain it briefly for abuse monitoring on a standard tier. The privacy page says exactly that rather than implying an absolute.

  5. Accessibility as correctness

    Body text is 19px and scales with the browser. check:contrast enforces WCAG AA for normal text on every colour pair (lowest 6.79:1) and a hard 17px minimum; axe-core reports zero violations on all four pages.

Result

Both screens, the analysis pipeline, the free-scan gate, signed entitlement tokens, Stripe checkout, recovery links, legal pages and security headers are built, with 54 assertions on Node’s test runner and a smoke test over every screen and API path.

The provider and Stripe calls are written and typed but have not yet run against a live key. The repository documents exactly what is verified and what is not.

Known limitations

  • Not yet exercised against live third-party APIs.
  • Legal copy still carries placeholders; check:legal fails until they’re replaced.

Stack

Next.js, TypeScript, React, Stripe, Zod, GitHub Actions