Building with Stripe Data Pipeline and the Neon backend 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. 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. Neon 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: - 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 - 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 For demo purposes, both apps apply a 100% discount at checkout, so you can complete a purchase without paying anything. In 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. Billing architecture Neither app has a Stripe webhook anywhere in its codebase. Both apps write to Stripe through its API and read every Stripe object from Postgres. Writing to Stripe and reading from Postgres Buying 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: Once 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. The 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. Computing credits with a SQL view Neither 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 . Handling refunds and chargebacks Because the app computes credits from Stripe data on every read, the query already handles the edge cases you'd normally write webhook handlers for: - 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: select purchased, spent, balance from app.credit balances where user id = $1; -- before the refund: purchased 40, spent 4, balance 36 -- after the refund: purchased 20, spent 4, balance 16 - Chargebacks: When a charge is disputed, disputed is set to true in the synced row and the view excludes the charge. - Double-crediting: Nothing can be credited twice, because no write ever adds credits. How the apps are built Headshot 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. The stack: - 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. - 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 . - Managed Better Auth https://neon.com/docs/auth/overview handles sign-up and sign-in. - Object Storage https://neon.com/docs/storage/overview has two private buckets, selfies and headshots. - 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. - AI Gateway https://neon.com/docs/ai-gateway/overview does the image generation, so there are no model provider keys in the app. - Next.js on Vercel serves the UI. Preventing double spending If 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: 1. Creating the job When 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 . 2. Calling a function on upload Object 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. 3. Claiming the job and spending credits In 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: select 1 from pg advisory xact lock hashtextextended $userId, 0 ; update app.jobs j set status = 'processing', started at = now from select coalesce select balance from app.credit balances where user id = $userId , 0 as balance a where j.id = $jobId and j.status = 'awaiting upload' and a.balance = j.cost returning j.id; The 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. Because the app computes credits from the jobs table, this design also handles three edge cases: - 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. - Redeliveries do nothing. A job only leaves awaiting upload once, so a duplicate trigger delivery finds nothing to claim. - Failed jobs return their credits. The view doesn't count failed jobs as spent, so no refund logic is needed. After 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. 4. Cleaning up stale jobs A 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. Debugging with Neon branches Because 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. A 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. A branch includes the whole backend: - 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. - Object Storage, including the files customers uploaded. - 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. Without 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. Debugging a failed job after payment Suppose 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: 1. Locate the failed job: Each row in app.jobs stores the ID of the trigger invocation that claimed it: select j.id, j.status, j.error, j.invocation id, j.created at from app.jobs j join neon auth."user" u on u.id = j.user id where u.email = 'customer@example.com' order by j.created at desc; 2. Review the function logs: Each log line includes invocation=… key=… , so you can filter the logs by the invocation ID from step 1: npx neon logs query --source function 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: npx neon branches create --name debug/headshots-1042 --parent 2026-10-08T10:42:00Z npx neon checkout debug/headshots-1042 pulls that branch's env into .env The branch contains the customer's user record, synced charge, job and uploaded selfie as they existed at 10:42. 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: npx neon dev serves onupload on :8787 scripts/replay-upload.sh uploads/