Recipe · checked November 13, 2026

Consumer web app with accounts and an optional paid plan

A web app for individual people, one person per account: a habit tracker, a journal, a budgeting tool, a recipe box, a hobby community. Next.js on Cloudflare Workers through the OpenNext adapter, Postgres on Neon, Better Auth with users in your own database, Stripe for a free-plus-paid model, and Resend for sign-in and account email.

Tool bills at 1,000 usersAbout $25/month
Setup steps9
Tools in the stack8

Fits when

  • Each account belongs to one person, not to a company or team.
  • It runs in a web browser (a PWA counts); phone apps are a separate recipe.
  • It's free to start, with an optional monthly or yearly paid plan, or no payments at all.

Not for

  • Products sold to companies, where several colleagues share an account (see the b2b-saas recipe).
  • iOS or Android apps that sell subscriptions inside the app (see the mobile-subscription-app recipe).
  • Products where users pay each other (see the marketplace recipe), and AI apps that answer from documents (see the ai-docs-app recipe).
  • Live collaboration (shared whiteboards, multiplayer editing): you need a realtime service this recipe doesn't set up.
  • Apps that store health, banking or children's data: the stack works, but read the compliance notes first.

The stack

CategoryPickWhyPick something else when
HostingCloudflare WorkersNext.js runs on Workers through the OpenNext adapter (@opennextjs/cloudflare), static files are served free, and Workers Paid is $5/month for 10 million requests, so a consumer app that gets popular stays cheap. There are no data transfer charges.
  • Vercel: You want native Next.js with no adapter, or you use Node middleware or the edge runtime. A paid product needs Pro ($20/month); Hobby is non-commercial.
  • Netlify: You already host the marketing site there.
DatabaseNeonPostgres that scales to zero, with a serverless driver that connects from Workers over HTTP, and database branches for previews.
  • Supabase: You want database, auth and file storage in one dashboard; free projects pause after a week with no activity.
AuthBetter AuthOpen-source sign-in that keeps users in your own Postgres, with no per-user fees: a consumer app can grow to a million free accounts without an auth bill.
  • Clerk: You want prebuilt sign-in screens and a user dashboard; free up to 50,000 retained users, but MFA and passkeys need a paid plan.
  • Supabase Auth: The database is Supabase.
PaymentsStripeCheckout in subscription mode for the paid plan, and the hosted customer portal so people can cancel or change card without writing to you.
  • Paddle: You sell to consumers in many countries and want someone else to collect and file sales tax and VAT (5% + 50¢ per sale).
EmailResendVerification, magic-link and password-reset email from your own domain, with React Email templates.
  • Postmark: Deliverability of sign-in email is the one thing you care about most.
MonitoringSentryError alerts with readable stack traces, so you hear about a broken sign-up before users give up.
  • Better Stack: You also want an uptime check and a public status page.
AnalyticsoptionalPostHogSign-up funnel, how many people come back in week two, and which free users upgrade.
  • Plausible: Page and referrer counts are enough, and you want no cookie banner.
StorageoptionalCloudflare R2Profile photos and user uploads on the same Cloudflare account, with no egress fees.
  • Vercel Blob: You host on Vercel instead and uploads are small.

About $25/month at 1,000 users in tool bills: Resend $20 + Cloudflare Workers $5; $0 for Neon, Better Auth, Stripe, Sentry. Workers Paid from launch ($5/month, 10M requests included): Free's 10 ms of CPU per request is tight for server-rendered pages. Usage profile: 1,000 monthly active users with light use: about 100,000 requests, 3,000 emails, 200,000 analytics events and 5 GB of stored files a month, and a database under 1 GB. AI apps add 50 questions per user a month at about 4,000 input and 500 output tokens each (200M input and 25M output tokens). Internal tools are priced for 10 staff instead.

Catches that change the plan

Setup steps

  1. Scaffold Next.js for Cloudflare Workers

    Create the app with Cloudflare's template, which sets up the OpenNext adapter, wrangler.jsonc and the preview and deploy scripts.

    • Create the app with the command below and pick TypeScript and the App Router.
    • Check that wrangler.jsonc has the nodejs_compat flag and a compatibility_date of 2024-09-23 or later.
    • Don't use the edge runtime: remove any `export const runtime = "edge"`, because the adapter doesn't support it. Node middleware (Next.js 15.2+) isn't supported yet either; use standard middleware.
    • On Windows, develop inside WSL: OpenNext says Windows itself works only at your own risk.
    • Add one env module that validates every variable at startup (zod), and commit a .env.example with names and no values.
    npm create cloudflare@latest -- my-app --framework=next --platform=workers
    npm run preview
    src/env.ts
    import { z } from "zod";
    
    export const env = z
      .object({
        DATABASE_URL: z.string().url(),
        BETTER_AUTH_SECRET: z.string().min(32),
        BETTER_AUTH_URL: z.string().url(),
        RESEND_API_KEY: z.string().startsWith("re_"),
        STRIPE_SECRET_KEY: z.string().startsWith("sk_").optional(),
        STRIPE_WEBHOOK_SECRET: z.string().startsWith("whsec_").optional(),
      })
      .parse(process.env);
    

    Done when: `npm run preview` serves the app locally on the Workers runtime, and it fails fast with a clear message when a variable is missing.

    Docs: opennext.js.org

  2. Set up Postgres with one owner per row

    Every row a person creates carries their user id, and every query filters by the signed-in user on the server.

    • Create a Neon project with a main branch for production and a dev branch for local work; use the pooled connection string.
    • Connect from the app with the Neon serverless driver, which works from Workers.
    • Add user_id to every table that holds a person's data, with an index and a foreign key to the users table.
    • Put all reads and writes behind one data-access layer that takes the session's user id, never an id sent from the browser.
    • Use a migration tool (Drizzle Kit or plain SQL files) and run migrations in CI, not by hand.
    npm i @neondatabase/serverless drizzle-orm
    npm i -D drizzle-kit

    Done when: A test signed in as user A gets nothing back when it asks for user B's rows by id.

    Docs: neon.com

  3. Add sign-in with Better Auth

    Email and password with verification, plus one social login (Google) is what most consumer apps need on day one.

    • Install Better Auth, point it at your Postgres with its Drizzle adapter, and generate its tables with the CLI.
    • Mount its handler on one Next.js route (app/api/auth/[...all]/route.ts) and read the session on the server with its API.
    • Turn on email verification and password reset, sending through Resend.
    • Add Google sign-in; it removes the biggest drop-off at sign-up.
    • Rate-limit sign-in, sign-up and password-reset routes (Better Auth has built-in rate limiting; check it is on in production).
    npm i better-auth
    npx @better-auth/cli@latest generate

    Done when: A new user can sign up, verify their email, sign out and sign back in, and a wrong password five times in a row gets rate-limited.

    Docs: better-auth.com

  4. Add the paid plan (skip if it's free)

    Stripe holds the subscription; your database mirrors it from webhooks, and features are gated on the server.

    • Create one Product with a monthly and a yearly Price in Stripe test mode.
    • Create one Stripe Customer per user and store stripe_customer_id on the user row.
    • Upgrade with a Checkout Session in subscription mode, with the user id in metadata.
    • Link to the Stripe customer portal for cancelling, plan changes and card updates; make cancelling as easy as signing up.
    • Handle webhooks in one route: verify the signature on the raw body, skip event ids you've already handled, then update the user's plan.
    • Gate paid features on the mirrored plan on the server, never on a client flag or the Checkout redirect.
    npm i stripe
    stripe listen --forward-to localhost:8787/api/stripe/webhook

    Done when: In test mode, a checkout unlocks the paid features, a cancel in the portal removes them at period end, and replaying the same webhook changes nothing.

    Docs: docs.stripe.com, docs.stripe.com

  5. Send account email from your domain

    Sign-in email that lands in spam is a sign-up you lose.

    • Verify a sending subdomain (for example mail.yourdomain.com) in Resend and add its SPF and DKIM records.
    • Add a DMARC record for the root domain, starting at p=none with reporting.
    • Write templates for verify email, magic link or password reset, and welcome.
    • Let Stripe send receipts; don't duplicate them.
    • Watch the free plan's 100 emails a day: a launch day of sign-ups passes it, so move to Pro before you announce.
    npm i resend @react-email/components

    Done when: A verification email arrives in the inbox (not spam) for a Gmail and an Outlook test account.

    Docs: resend.com

  6. Keep secrets out of git

    One copy of each secret per environment, synced to the host.

    • Keep development, preview and production values in a secrets manager (Infisical) and sync production to the Worker's secrets.
    • Use Stripe test keys everywhere except production.
    • Add a pre-commit secret scan (gitleaks) and rotate any key that was ever committed.
    npx wrangler secret put BETTER_AUTH_SECRET

    Done when: A fresh clone runs with secrets pulled from the secrets manager, and git history has no .env file.

  7. Add monitoring and analytics

    Know when sign-up breaks, and which free users come back and upgrade.

    • Install Sentry for Next.js and upload source maps in production builds.
    • Tag Sentry events with the user id, never the email address.
    • Add an uptime check on a /api/health route.
    • Add PostHog with a sign-up funnel and a week-two retention chart; ask for cookie consent first for EU visitors, or use cookieless mode.
    npx @sentry/wizard@latest -i nextjs
    npm i posthog-js

    Done when: A thrown test error shows up in Sentry with a readable stack trace and the user id, and no email address.

    Docs: docs.sentry.io

  8. Let people leave with their data

    Consumers ask to export or delete their account; privacy laws in the EU, UK and California give them the right to.

    • Add a settings page with Export my data (a JSON or CSV download) and Delete my account.
    • Delete or anonymize every row with that user id, their uploads in storage, and the Stripe customer (after cancelling any subscription).
    • Publish a privacy policy that lists every vendor that gets personal data (this stack: Cloudflare, Neon, Resend, Stripe, Sentry, PostHog).

    Done when: Deleting a test account leaves no rows, files or Stripe customer behind for that user.

  9. Deploy to production

    Move to Workers Paid before launch and deploy from main.

    • Switch the account to Workers Paid ($5/month): Free stops at 100,000 requests a day and allows 10 ms of CPU per request, which a server-rendered page can pass.
    • Watch the Worker size in the deploy output: the gzip size must stay under 10 MiB on Paid (3 MiB on Free).
    • Add your domain to the Worker as a custom domain in the Cloudflare dashboard.
    • Deploy from CI on every push to main, with a Neon branch per pull request for previews.
    npm run deploy

    Done when: The production domain serves the app over HTTPS, sign-up works end to end, and a deploy from main goes out without a manual step.

    Docs: opennext.js.org, developers.cloudflare.com

Best practices

  • Filter every query by the signed-in user's id on the server; never trust an id from the browser.
  • Treat Stripe as the source of truth for the paid plan and mirror it from webhooks.
  • Make cancelling a subscription as easy as starting one, with the Stripe customer portal one click from settings.
  • Offer Google sign-in next to email: fewer people quit at the sign-up form.
  • Build export and delete-my-account before launch, not after the first request.
  • Keep personal data out of logs and error reports; tag errors with the user id only.

Pitfalls

  • Unlocking the paid plan on the Checkout redirect instead of on the webhook; redirects can be skipped or faked.
  • Launching on Workers Free and hitting the 100,000-requests-a-day cap (error 1027) on the day you get shared.
  • Using `runtime = "edge"` or Node middleware, which the OpenNext adapter doesn't support yet.
  • Sending sign-in email from the free Resend plan on launch day and stopping at 100 a day.
  • Loading analytics cookies for EU visitors before they agree.

Costs and obligations

These point to obligations to check; they aren't legal or tax advice.