Skip to main content
Example Build9 min read

Example build: a small subscription app

A teaching walkthrough of a paid-accounts app: what the AI generated, what was actually happening underneath, and the four problems that appeared once a second account existed.

The idea

A small app where people sign in, save their own records, and pay monthly for a higher limit. It is the simplest shape that still contains accounts, data ownership, payments and email — the four places beginners get caught.

Architecture

  • A browser app rendered on the server, so pages are readable before any JavaScript runs.
  • Server functions for anything needing a secret: payment sessions, email sending, webhook handling.
  • A hosted database with row-level access rules as the real security boundary.
  • A payment provider holding the subscription state, mirrored into the database by a webhook.

Tools

ToolRole
LovableAI building platform that generated and edited the application code
GitHubVersion history, so every AI change is reviewable as a diff
VercelBuilds each commit and serves the public site
SupabaseDatabase, accounts and row-level access rules
StripeSubscriptions, checkout and the billing webhook

The build process

  • Described the data first — users, records, plan — before asking for any screens.
  • Generated accounts and the record list, then immediately created a second test account.
  • Added checkout as a server function, with the secret key read inside the handler.
  • Added the webhook that flips the account to paid, with signature verification from the start.
  • Walked the whole flow signed out, in a private window, before touching styling.

What the AI generated

  • Database tables for records and subscriptions, with owner columns and timestamps.
  • Access rules limiting reads and writes to the owning account.
  • A checkout endpoint creating a session and returning a redirect URL.
  • A webhook endpoint updating the account's plan.
  • List, create and delete screens wired to the database.

What was happening underneath

  • Every list query was being filtered twice: once by the code and once by the database rules. Only the second one is a security control.
  • The checkout endpoint was the only place the secret key existed; the browser never saw it.
  • The webhook was the true source of plan changes — the redirect back from checkout only meant the user returned, not that payment succeeded.
  • Session tokens were attached automatically by middleware, which is why a single removed line could produce 401s everywhere.

Problems, and what they actually were

The record list was empty for the second account, with no error.

Underneath: Access rules were enabled and the read policy targeted the wrong role, so the query legitimately matched nothing.

Fix: Rewrote the policy to name the authenticated role and compare the owner column, then retested with both accounts.

Payment succeeded and the account stayed on the free plan.

Underneath: The redirect after checkout updated the interface optimistically, while the webhook that actually records payment was failing signature verification.

Fix: Used the raw request body for verification instead of the parsed one, and made the interface read plan state from the database rather than from the redirect.

Welcome emails never arrived.

Underneath: The sending domain was not verified, so the provider rejected every send with a clear error the code was swallowing.

Fix: Surfaced the provider's error text, verified the domain, and made the retry key vary so a failed send could be attempted again.

Everything worked in preview and broke on the live domain.

Underneath: The payment keys and the callback URL were set in one environment only.

Fix: Set each variable per environment and derived the callback URL from the request origin.

Deployment

  • Merged to the production branch and watched the build log rather than assuming.
  • Confirmed the domain pointed at the newest successful deployment.
  • Ran one real payment on the live site and confirmed the plan changed in the database.
  • Checked function logs during the walkthrough, not just the visible result.

Lessons learned

  • A second account is the cheapest security test that exists, and it finds the expensive bugs.
  • Payment state belongs to the webhook, never to the redirect.
  • Swallowed error messages cost more time than the original bug.
  • Configuration, not code, causes most production-only failures.

Read the concepts behind it