Sign in, pay, brief: the server is the only source of truth

A paid client portal on a static site: identity-keyed checkout, entitlements checked on the server, and briefs that are held, never refused, when something is missing.

Role
Architecture and full-stack build
Period
2026
Status
In production
Stack
  • Cloudflare Pages Functions
  • Firebase Auth
  • Supabase
  • Flutterwave
  • n8n
  • JavaScript
  • Python
Paid client portal: identity-keyed checkout and server-checked briefs Sequence diagram between Client, Static site, Pages Function, Postgres, n8n, Payment provider. 1. Client to Static site: sign in (Firebase) 2. Static site to Pages Function: checkout + ID token 3. Pages Function to Pages Function: verify token with Google 4. Pages Function to Payment provider: mint link (uid, plan) 5. Payment provider to Client: hosted checkout 6. Payment provider to n8n: signed webhook 7. n8n to Postgres: extend paid period 8. Client to Pages Function: submit brief + token 9. Pages Function to Postgres: re-read plan 10. Pages Function to Postgres: dated, or held: gap named 11. Pages Function to n8n: notify 12. n8n to Client: acknowledgement sign in (Firebase) checkout + ID token verify token with Google mint link (uid, plan) hosted checkout signed webhook extend paid period submit brief + token re-read plan dated, or held: gap named notify acknowledgement Display may trust the browser. Submission never does. Client Static site Pages Function Postgres n8n Payment provider
Checkout and brief submission both start by verifying the ID token. The payment lands on the signed-in account, and every submission re-reads the plan from the database.

The problem

Buyers sign in with one email and pay with another, and hosted payment links carry no customer. Paying clients also need to brief work without a product page, on a site that is entirely static.

The system

  1. Checkout links are minted server-side only after Google verifies the Firebase ID token; the user id, plan and product line travel with the payment.
  2. The payment webhook is signature-verified, idempotent under retries, and extends the paid period instead of overwriting it.
  3. Every brief submission re-reads the plan from Postgres. A user can edit their own profile document, so display may trust the browser but submission never does.
  4. An incomplete brief is held with the missing information named, and its due date starts when it becomes complete. Turnaround comes from one business-day module shared by Python and JavaScript.

Key decisions

One pricing path for single requests and mixes

WhyPriced separately, a mix would have been cheaper than its parts, and every client would learn to use it.

Trade-offA more complex pricing function with its own parity tests.

Hold, never refuse, an incomplete brief

WhyRequiring a product link blocked clients who had no product page at all.

Trade-offHeld briefs need a nudge and a place in the ops digest.

Refuse credentials in any free-text field

WhyPeople paste API keys into forms. Key-shaped strings are rejected in the browser and again on the server.

Trade-offOccasional false positives on long random strings.

Results

  • 100+ API contract checks
  • Python ↔ JavaScript parity tests for billing and business-day maths
  • Access rules proven by probing the database from outside with the public key

What I’d do next

File uploads to object storage with signed URLs, and a nudge for briefs left incomplete.