Stripe to Postgres in 5 Minutes: A Free F-Pulse Tutorial
If you take payments through Stripe and ever need to ask a question like "what's our daily revenue by plan over the last 60 days" or "which customers churned in March", you want that data in your own database. The Stripe dashboard is fine for one-off lookups; SQL against your warehouse is fine for everything else.
This tutorial walks through syncing Stripe data into Postgres using F-Pulse OSS in about five minutes. No Python, no Airflow, no Fivetran bill. Free under Apache 2.0.
Why pull Stripe into Postgres
Three concrete reasons:
- You can join Stripe data with your application data. Your
userstable is in Postgres; yourcustomersfrom Stripe live in Stripe. Joining them in BI requires both being in the same place. - You can write arbitrarily complex SQL. Stripe's API filters are limited; SQL isn't. Cohort analyses, multi-period comparisons, churn signals — these need a warehouse, not an API.
- You stop paying per-row. Hosted ETL (Fivetran et al.) bills on Monthly Active Rows. A free self-hosted pipeline doesn't.
Prerequisites
You need three things running:
- Stripe API key — restricted key with read access to charges, customers, subscriptions. Get it from Stripe Dashboard → Developers → API keys → Create restricted key.
- Postgres — any reachable instance. Local Docker, RDS, Supabase, Neon, your own server. Need credentials with
CREATE TABLEpermission on at least one schema. - F-Pulse OSS — Apache 2.0, free, runs on Docker.
To stand up F-Pulse:
git clone https://github.com/hybridyn/fpulse-oss
cd fpulse-oss
docker compose up -d
Wait ~30 seconds for the services to come up. Then open http://localhost:5174 and create the bootstrap admin account.
Step 1 — Save the connections
F-Pulse separates connections (credentials, stored once) from nodes (used in pipelines). Save both connections first.
In the F-Pulse UI, go to Settings → Credentials → Add Connection:
Stripe connection:
- Connector type:
stripe - API key: your restricted Stripe key (paste it; F-Pulse encrypts it at rest with Fernet — AES-128-CBC + HMAC-SHA256, master key at
~/.fpulse/secret.key, chmod 600) - Test connection → should return
okwith a sample list of available streams
Postgres connection:
- Connector type:
postgresql - Host, port, database, user, password
- Test connection → should return
okwith the list of accessible schemas
Both connections are now reusable across any pipeline.
Step 2 — Build the pipeline visually
Go to Pipelines → New Pipeline. You land on the visual canvas.
Node 1: SaaS Connector (Stripe Charges)
Drag a SaaS Connector node from the left rail onto the canvas. In the side panel:
- Connector: select your Stripe connection
- Stream:
charges(Stripe's primary transaction stream) - Incremental cursor:
created(Stripe charges have acreatedUnix timestamp) - Sync mode:
incremental(only pull rows newer than the last successful run) - Page size: 100 (Stripe's API max)
Click the node. The Output panel at the bottom shows a sample of the data F-Pulse will pull on this run. You can flip between Table, Schema, and JSON views.
The schema will look something like:
id text (Stripe charge ID — primary key)
amount bigint (cents)
currency text
customer text (Stripe customer ID — foreign key)
status text (succeeded / failed / pending)
created timestamp
description text
metadata jsonb
Node 2: Schema Mapper
Drag a Schema Mapper node and wire the Stripe output into it. The Schema Mapper lets you rename columns, drop ones you don't need, and coerce types without writing SQL. Useful here because Stripe's amount is in cents (bigint) and you'll probably want a amount_decimal column too.
In the side panel, configure:
- Map
amount→amount_cents(rename for clarity) - Add derived column:
amount_decimal = amount / 100.0(type: numeric(12,2)) - Drop
descriptionif you don't need it - Drop
metadataif you don't need it (or keep as jsonb — Postgres handles it natively)
Node 3: Database Sink (Postgres)
Drag a Database Sink node and wire the Schema Mapper output into it. In the side panel:
- Connection: select your Postgres connection
- Schema:
stripe(F-Pulse will create it if missing — assuming the user has permission) - Table:
charges - Write mode:
upsert— this is the critical setting for idempotency - Primary key:
id(Stripe charge ID is globally unique)
With upsert mode on id, re-running the pipeline never produces duplicates. If a charge gets refunded later and Stripe updates the status, the next run picks it up and overwrites the existing row.
Node 4: Schedule
Drag a Schedule node into the canvas (it doesn't need to be wired — it's a trigger). In the side panel:
- Cron expression:
/15 *(every 15 minutes) - Timezone:
UTC(recommended for warehouse work) - Enabled: yes
If you want daily-only instead of every 15 minutes, use 0 6 * for 6am UTC.
Step 3 — Test before deploying
Click Run Once at the top right. F-Pulse executes the pipeline end-to-end with the current settings. The execution log shows row counts at each step:
✓ Stripe SaaS Connector — 142 rows pulled in 1.8s
✓ Schema Mapper — 142 rows mapped in 12ms
✓ Postgres Database Sink — 142 rows upserted in 340ms (142 inserted, 0 updated)
Open your Postgres database and check:
SELECT count(*), max(created)
FROM stripe.charges;
Should return the count and the latest charge timestamp. The pipeline is working end-to-end.
Step 4 — Deploy
Once the test run is clean, click Deploy at the top right. F-Pulse takes a SHA-256 snapshot of the pipeline IR, signs it for tamper detection, and registers the schedule. The next run will fire automatically at the next cron tick.
The pipeline is now in production. Re-runs are idempotent (upsert on id), incremental (only pulls new charges via the created cursor), and audited (every run captured with row counts, duration, peak memory, CPU seconds).
Bonus: Add a Data Profile node
Drag a Data Profile node and wire the Stripe Source output into it (in addition to the existing Schema Mapper). The profile node emits one row per column with summary statistics: null %, distinct count, min/max, top value.
This is gold for catching upstream changes early. If Stripe ever ships a new charge status value or starts returning a column with 100% nulls, the profile will surface it before bad data flows downstream.
You can pipe Data Profile output into a Database Sink targeting a separate metadata.charges_profile table — then build a Grafana panel that watches for null-rate or distinct-count drift.
Repeat for Customers and Subscriptions
The same pattern handles the other two main Stripe streams:
- Customers — Stream:
customers, cursor:created, primary key:id, target:stripe.customers - Subscriptions — Stream:
subscriptions, cursor:created, primary key:id, target:stripe.subscriptions
You'll want all three tables to support the joins that make this exercise worthwhile:
SELECT
date_trunc('day', c.created) AS day,
s.plan_id,
COUNT(*) AS charge_count,
SUM(c.amount_decimal) AS revenue
FROM stripe.charges c
JOIN stripe.customers cust ON c.customer = cust.id
JOIN stripe.subscriptions s ON cust.id = s.customer
WHERE c.status = 'succeeded'
AND c.created > now() - interval '60 days'
GROUP BY 1, 2
ORDER BY 1 DESC, revenue DESC;
That query was impossible before — Stripe's API can't do multi-stream joins. Now it's a sub-second SQL query against your own Postgres.
What you can't do in OSS
Be honest about the line:
- Log-based CDC is F-Pulse+. For Stripe this doesn't matter (Stripe is API-only, not a database) — but if your source were Postgres-to-Postgres replication with sub-second latency, you'd want Plus's CDC Source node.
- Multi-worker horizontal scaling is Plus. For a single Stripe pipeline at 15-minute cadence, a single worker is fine.
- Workspace RBAC with multiple users on shared pipelines is Plus. For a solo developer's setup, OSS is fully featured.
- Production-grade Salesforce connector is Plus. The OSS Salesforce manifest is the starter version (REST + cursor pagination, partial Account schema, no Bulk API 2.0).
For Stripe → Postgres specifically, OSS is the full answer.
What happens on every subsequent run
The pipeline is now self-maintaining:
- Schedule fires (every 15 minutes)
- F-Pulse opens the Stripe connection, queries
chargeswithcreated > - New charges (if any) flow through Schema Mapper to the Postgres Sink
- Upsert mode merges by
id— new rows inserted, existing rows updated if status changed - Cursor advances to the latest
createdseen - Run is logged: status, duration, row counts, peak memory, IR snapshot hash
If a run fails (network blip, Postgres down), F-Pulse retries with exponential backoff. If the retries exhaust, you get an alert (in-app, email, Slack, Discord, or webhook — your choice in Settings).
The bottom line
Stripe → Postgres is the most common SaaS-to-warehouse pipeline a small team builds. It's the proof-of-life for any data stack. Five minutes, $0 in tool cost, fully idempotent, self-monitoring.
If this is the first F-Pulse pipeline you build, the same pattern handles HubSpot, Salesforce, Jira, Notion, GitHub, and the other 25 SaaS connectors in OSS. The pipeline shape is identical — swap the source, keep the upsert + schedule structure.
F-Pulse OSS ships 37 connectors including Stripe, Postgres, and the visual builder in one Docker image. Get the full stack in 3 minutes.
Build data pipelines visually
F-Pulse is open source. Try it in under 3 minutes.