{"slug": "building-with-stripe-data-pipeline-and-the-neon-backend", "title": "Building with Stripe Data Pipeline and the Neon backend", "summary": "Stripe released real-time sync to Postgres for Stripe Data Pipeline, streaming every Stripe API object with a list endpoint — including charges, customers, prices, invoices and subscriptions — into a customer's own Postgres database within a few seconds. Neon, one of the supported destinations, built two example apps on the feature: Headshot Studio, an AI headshot generator where one credit buys one headshot, and Bouncer, a prepaid content moderation API that bills per item checked; both apps run without any Stripe webhook, writing to Stripe through its API and reading Stripe objects from Postgres, and both apply a 100% discount at checkout for demo purposes. The apps compute customer credits as a SQL view of purchases minus spending rather than storing a balance, so a half refund on the 40-credit $29 Pro pack moves a balance from 36 to 16 once amount_refunded syncs.", "body_md": "Stripe just released [real-time sync to Postgres for Stripe Data Pipeline](https://neon.com/blog/stripe-data-pipeline-realtime-sync-to-postgres), which streams your Stripe data into your own Postgres database within a few seconds. Every object in the Stripe API with a list endpoint, such as charges, customers, prices, invoices and subscriptions, syncs into a regular table you can join against.\n\nNeon is [one of the supported destinations](https://docs.stripe.com/data/data-pipeline/real-time-sync-to-postgres/configure?postgres-provider=neon), so we built two example apps on it right away:\n\n- **Headshot Studio:** An AI headshot generator. Users buy credit packs, upload a selfie, and receive professional studio headshots.[Try it](https://neon-stripe-sync-headshot-studio.vercel.app/) ·[Code](https://github.com/rishi-raj-jain/neon-stripe-sync-realtime-headshot-studio)\n- **Bouncer:** A prepaid content moderation API that bills customers for each item it checks.[Try it](https://neon-stripe-sync-realtime-bouncer.vercel.app/) ·[Code](https://github.com/rishi-raj-jain/neon-stripe-sync-realtime-bouncer)\n\nFor demo purposes, both apps apply a 100% discount at checkout, so you can complete a purchase without paying anything.\n\nIn this post, we walk through how we built both apps, and then show how Stripe Data Pipeline and Neon branches help you debug billing issues in them.\n\n## Billing architecture\n\nNeither app has a Stripe webhook anywhere in its codebase. Both apps write to Stripe through its API and read every Stripe object from Postgres.\n\n### Writing to Stripe and reading from Postgres\n\nBuying credits starts a normal [Checkout Session](https://docs.stripe.com/api/checkout/sessions). The app adds the user ID and credit count as metadata on the payment, and that metadata syncs into Postgres with the charge. Here's the Headshot Studio code:\n\nOnce the payment goes through, Stripe syncs the charge into `stripe.charges` within a few seconds, and the dashboard polls Postgres until the new credits appear. No webhook handler needs to run in between.\n\nThe apps also read prices and promotion codes from the synced tables. Neither app hardcodes a price ID. They look up prices by `lookup_key` in `stripe.prices` and promotion codes by code in `stripe.promotion_codes`, so when you switch from a sandbox to live mode, or to another Stripe account, you don't need to update any IDs in the code.\n\n### Computing credits with a SQL view\n\nNeither app stores a balance. A customer's credits are a SQL view: what they bought (from the synced Stripe tables) minus what they spent (from the app's own tables).\n\n### Handling refunds and chargebacks\n\nBecause the app computes credits from Stripe data on every read, the query already handles the edge cases you'd normally write webhook handlers for:\n\n- \n**Refunds:** Suppose a customer bought the 40-credit Pro pack for $29 and you refund half of it in the Stripe Dashboard. A few seconds later,`amount_refunded` syncs, and the same query returns the new number:\n\n```\nselect purchased, spent, balance from app.credit_balances where user_id = $1;\n-- before the refund:  purchased 40, spent 4, balance 36\n-- after the refund:   purchased 20, spent 4, balance 16\n```\n\n- \n**Chargebacks:** When a charge is disputed,`disputed` is set to`true` in the synced row and the view excludes the charge.\n- \n**Double-crediting:** Nothing can be credited twice, because no write ever adds credits.\n\n## How the apps are built\n\nHeadshot Studio is an AI headshot generator paid for with credits. You buy a credit pack (one credit buys one headshot) and upload a selfie directly from the browser to a private bucket. A few moments later, the headshots appear on the studio page.\n\nThe stack:\n\n- [**Stripe Data Pipeline**](https://docs.stripe.com/data/data-pipeline/real-time-sync-to-postgres) syncs`customers` ,`charges` ,`payment_intents` ,`checkout_sessions` ,`prices` and`promotion_codes` into a stripe schema.\n- [**Lakebase Postgres**](https://neon.com/docs/postgres/overview) holds three schemas:`stripe.*` (owned by the pipeline),`neon_auth.*` (users and sessions) and`app.*` (the app's own tables).\n- [**Managed Better Auth**](https://neon.com/docs/auth/overview) handles sign-up and sign-in.\n- [**Object Storage**](https://neon.com/docs/storage/overview) has two private buckets, selfies and headshots.\n- [**Functions**](https://neon.com/docs/compute/functions/overview) and[** triggers**](https://neon.com/docs/compute/functions/triggers/overview) run`onupload` when a selfie is uploaded, and`sweeper` on a five-minute cron.\n- [**AI Gateway**](https://neon.com/docs/ai-gateway/overview) does the image generation, so there are no model provider keys in the app.\n- Next.js on Vercel serves the UI.\n\n#### Preventing double spending\n\nIf there's no balance column to decrement, what stops someone from spending the same credits twice? Headshot Studio prevents it with four parts of the stack:\n\n**1. Creating the job**\n\nWhen you click generate, the API inserts a row in `app.jobs` with status `awaiting_upload` and returns a presigned upload URL. The browser uploads the selfie directly to a private [Object Storage](https://neon.com/docs/storage/overview) bucket, without passing through the app's server. The object key includes the user and job IDs (`uploads/{userId}/{jobId}.jpg`).\n\n**2. Calling a function on upload**\n\nObject Storage can call a [Neon Function](https://neon.com/docs/compute/functions/overview) whenever a new object is uploaded, using a [storage trigger](https://neon.com/docs/compute/functions/triggers/object-storage). The trigger sends the bucket and object key to `onupload`, and since the key contains both IDs, the function knows whose job it is without a queue or an extra lookup.\n\n**3. Claiming the job and spending credits**\n\nIn one Postgres transaction, `onupload` takes a per-user advisory lock and sets the job to `processing` only if the credits view says the user can afford it:\n\n```\nselect 1 from pg_advisory_xact_lock(hashtextextended($userId, 0));\n\nupdate app.jobs j\n   set status = 'processing', started_at = now()\n  from (select coalesce((select balance from app.credit_balances where user_id = $userId), 0) as balance) a\n where j.id = $jobId\n   and j.status = 'awaiting_upload'\n   and a.balance >= j.cost\nreturning j.id;\n```\n\nThe balance it checks comes from `app.credit_balances`, which reads the purchases that [Stripe Data Pipeline](https://docs.stripe.com/data/data-pipeline/real-time-sync-to-postgres) syncs into Postgres. Once the job is `processing`, it counts as spent in that same view, so the update both checks the balance and spends it.\n\nBecause the app computes credits from the jobs table, this design also handles three edge cases:\n\n- Two uploads at once can't overspend. The advisory lock makes one user's claims run one at a time, so the second sees the first one's debit.\n- Redeliveries do nothing. A job only leaves `awaiting_upload` once, so a duplicate trigger delivery finds nothing to claim.\n- Failed jobs return their credits. The view doesn't count failed jobs as spent, so no refund logic is needed.\n\nAfter the claim, `onupload` reads the selfie, calls [AI Gateway](https://neon.com/docs/ai-gateway/overview) once per variant, writes the results to the headshots bucket and marks the job succeeded.\n\n**4. Cleaning up stale jobs**\n\nA second function, `sweeper`, runs every five minutes on a [schedule trigger](https://neon.com/docs/compute/functions/triggers/schedule). It fails jobs stuck in `processing` for over 20 minutes and expires jobs that never got an upload, so the view returns those credits to the balance.\n\n## Debugging with Neon branches\n\nBecause your Stripe data is in [Lakebase Postgres](https://neon.com/docs/postgres/overview) as part of a Neon backend, you can create a branch that contains both your Stripe data and your app data.\n\nA Neon [branch](https://neon.com/docs/introduction/branching) is an isolated copy of your project, created in seconds without duplicating any data up front. It shares storage with its parent and stores only the changes made on the branch. You can also create a branch from a past point in time within your project's [history window](https://neon.com/docs/postgres/backup-restore/history-window), which lets you inspect production data as it was when a customer reported a bug.\n\nA branch includes the whole backend:\n\n- Postgres, including the `neon_auth` users and the synced`stripe.*` rows. The customer's purchases, refunds and invoices appear in the branch as they were synced at that point in time.\n- Object Storage, including the files customers uploaded.\n- Triggers, copied but disabled. Inherited triggers don't run on a child branch until you enable them, so a debug branch of Headshot Studio can't process uploads on its own, and a debug branch of Bouncer can't charge anyone's card.\n\nWithout Stripe Data Pipeline, you would collect the billing details of a bug report manually from the Stripe Dashboard. With it, the billing data, app data and files are in one place that you can copy, query and modify without affecting production.\n\n### Debugging a failed job after payment\n\nSuppose a customer reports that they bought the Pro pack and uploaded a selfie, but received no headshots. The following steps show how to investigate the issue on a branch:\n\n**1. Locate the failed job:** Each row in `app.jobs` stores the ID of the trigger invocation that claimed it:\n\n```\nselect j.id, j.status, j.error, j.invocation_id, j.created_at\n  from app.jobs j\n  join neon_auth.\"user\" u on u.id = j.user_id\n where u.email = 'customer@example.com'\n order by j.created_at desc;\n```\n\n**2. Review the function logs:** Each log line includes `invocation=… key=…`, so you can filter the logs by the invocation ID from step 1:\n\n```\nnpx neon logs query --source function\n```\n\n**3. Create a branch at the time of the failure:** Use the job's timestamp as the branch point, then connect your local environment to the new branch:\n\n```\nnpx neon branches create --name debug/headshots-1042 --parent 2026-10-08T10:42:00Z\nnpx neon checkout debug/headshots-1042   # pulls that branch's env into .env\n```\n\nThe branch contains the customer's user record, synced charge, job and uploaded selfie as they existed at 10:42.\n\n**4. Reproduce the failure locally:** [neon dev](https://neon.com/docs/cli/dev) runs `onupload` on your machine against the branch's data and files. A script sends the same JSON payload that the storage trigger sent:\n\n```\nnpx neon dev                                            # serves onupload on :8787\nscripts/replay-upload.sh uploads/<userId>/<jobId>.jpg   # in another terminal\n```\n\nTo run the function again, reset the job's status on the branch:\n\n```\nupdate app.jobs\n   set status = 'awaiting_upload', error = null, started_at = null, finished_at = null\n where id = '<jobId>';\n```\n\n**5. Identify and fix the cause:** In this case, the selfie was an iPhone HEIC photo that the image pipeline couldn't process. The fix adds a check that detects the file type and asks the user to upload a JPEG instead. Rerun the upload until the job succeeds. The function writes the generated headshots to the branch's headshots bucket, so production data isn't affected.\n\n**6. Deploy the fix and delete the branch:** On production, the failed job has already returned its credits, so the customer can upload again.\n\n```\nnpx neon branches delete debug/headshots-1042\n```\n\n### Testing refunds against the credit balance\n\nA branch also lets you test the balance view against production data without involving Stripe. On a debug branch, simulate a partial refund by updating the synced charge:\n\n```\n-- debug branch only: on main, stripe.* belongs to the pipeline\nupdate stripe.charges set amount_refunded = amount / 2 where id = 'ch_…';\nselect * from app.credit_balances where user_id = '<userId>';\n```\n\nThe balance decreases by half the pack immediately, with no webhook handler involved. Setting `disputed = true` removes the charge from the view entirely. To confirm the behavior end to end on main, refund a test-mode charge in the Stripe Dashboard, check that `stripe.charges._updated_at` changes, and verify that the balance updates within seconds.\n\n### Debugging a job timeout\n\nSuppose a customer reports that the sweeper marked their job as failed while it was still running. Each sweeper run logs its `scheduledAt` timestamp, so you can create a branch at that timestamp and query the data the sweeper read:\n\n```\nnpx neon branches create --name debug/sweeper --parent 2026-10-08T11:05:00Z\nselect id, status, started_at, now() - started_at as running_for\n  from app.jobs where status = 'processing';\n```\n\nIf a four-variant job legitimately ran for more than 20 minutes, the timeout is too short. Increase it, run `neon deploy` on the branch to enable its triggers, and confirm that the next scheduled run behaves correctly before you deploy the change to production.\n\n## Building your own app\n\nYou can use the same pattern for any product that sells credits, usage or access: write to the Stripe API, read from the synced `stripe.*` tables, and compute billing state with a SQL view.\n\nTo build your own app with a coding agent, run `npx neon@latest init` in your project first so the agent has the Neon MCP server and skills. Then paste a prompt like this:\n\n```\nBuild a <describe your app> that bills customers through Stripe, using Stripe Data Pipeline's real-time sync to Postgres and the Neon backend. Use these repos as reference for the billing pattern:\n\n- https://github.com/rishi-raj-jain/neon-stripe-sync-realtime-headshot-studio\n- https://github.com/rishi-raj-jain/neon-stripe-sync-realtime-bouncer\n\nFollow the same rules:\n\n- No Stripe webhooks. Write to Stripe only through its API, and read every Stripe object from the synced stripe.* tables.\n- Keep each customer's balance as a SQL view: purchases from the synced Stripe tables minus usage from the app's own tables. Never store or increment a balance.\n- Look up prices and promotion codes in stripe.prices and stripe.promotion_codes by lookup key or code. Don't hardcode IDs.\n- Declare the backend in neon.ts (Neon Auth, Functions, triggers, AI Gateway or Object Storage as needed) and provision it with neon deploy.\n- Add indexes on the synced Stripe tables for the per-customer lookups the app runs.\n\nBefore writing code, list the Stripe objects the app needs synced and sketch the balance view.\n```\n\nIf you have questions, you can find us on [Discord](https://neon.com/discord).", "url": "https://wpnews.pro/news/building-with-stripe-data-pipeline-and-the-neon-backend", "canonical_source": "https://neon.com/blog/building-with-stripe-data-pipeline-and-neon-backend", "published_at": "2026-10-26 12:00:00+00:00", "updated_at": "2026-10-27 13:44:41.621240+00:00", "lang": "en", "topics": ["ai-products", "ai-tools", "developer-tools", "ai-infrastructure"], "entities": ["Stripe", "Neon", "Stripe Data Pipeline", "Headshot Studio", "Bouncer", "Lakebase Postgres"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/building-with-stripe-data-pipeline-and-the-neon-backend", "markdown": "https://wpnews.pro/news/building-with-stripe-data-pipeline-and-the-neon-backend.md", "text": "https://wpnews.pro/news/building-with-stripe-data-pipeline-and-the-neon-backend.txt", "jsonld": "https://wpnews.pro/news/building-with-stripe-data-pipeline-and-the-neon-backend.jsonld"}}