Data Pipeline
Data Pipeline — Neon → BigQuery (dbt)
The data pipeline syncs Neon (OLTP / application database — the source of truth) into BigQuery (OLAP / analytics warehouse). Neon is where the app reads and writes; BigQuery is where analytics, reporting, and data-moat modeling run. Data flows app → warehouse (the reverse of the older Supabase-era pipeline).
Architecture
App (Cloud Run) ──writes──▶ Neon (Postgres · OLTP · source of truth)
│
extract-load (incremental, nightly)
▼
BigQuery (raw_*)
│
dbt (transform, in BigQuery)
▼
BigQuery (marts) ──▶ BI / analytics / ML
- The app reads/writes Neon for low-latency OLTP.
- A scheduled extract-load job copies changed rows from Neon into BigQuery
raw_*tables — incrementally,WHERE updated_at > <watermark>. - dbt (BigQuery adapter) transforms
raw_*→martswithin BigQuery.
dbt runs against a single warehouse (BigQuery) — it does not read Postgres and write BigQuery in one hop. The Neon → BigQuery move is the extract-load step; dbt then models inside BigQuery. This replaces the old Supabase Wrappers FDW, which does not exist on Neon.
Directory Structure
dbt/
├── dbt_project.yml
├── profiles.yml.example # BigQuery target
├── models/
│ ├── staging/
│ │ └── stg_patients.sql # cleans raw_patients (from the Neon extract)
│ └── marts/
│ └── active_patients.sql # incremental mart, materialized in BigQuery
└── README.md
jobs/
└── neon_to_bq.py # extract-load: changed Neon rows → BigQuery raw_*
Extract-Load — neon_to_bq.py
Reads changed rows from Neon and loads them into BigQuery raw tables, watermarked to minimize egress:
# conceptual — pulled from Neon each run, then loaded into BigQuery raw_*
SELECT * FROM patients WHERE updated_at > :last_watermark;- Runs as a scheduled Cloud Run Job (
neon-to-bq) — a standalone job, not part of thebackend/workerservice. Cloud Scheduler (or the nightlydbt.ymlworkflow) triggers it. - Loads into BigQuery
raw_*via thegoogle-cloud-bigqueryclient. - Persists the watermark (max
updated_at) so each run transfers only new/changed rows.
Cross-Cloud Egress
Neon runs in AWS us-west-2; BigQuery in GCP. The WHERE updated_at > watermark keeps the Neon → BigQuery transfer incremental — only new/changed records cross the boundary. Full-table cross-cloud scans are prohibited (egress cost + compliance risk).
PHI in the warehouse: either de-identify PHI as it lands in BigQuery, or keep the BigQuery dataset inside the BAA with restricted service-account access. See HIPAA Compliance.
Key Model — active_patients.sql (in BigQuery)
Incremental materialization minimizes reprocessing on each run:
{{ config(materialized='incremental', unique_key='patient_id') }}
SELECT
patient_id,
first_name,
last_name,
date_of_birth,
updated_at
FROM {{ ref('stg_patients') }}
{% if is_incremental() %}
WHERE updated_at > (SELECT MAX(updated_at) FROM {{ this }})
{% endif %}Scheduling — GitHub Actions
The pipeline runs nightly via .github/workflows/dbt.yml:
| Setting | Value |
|---|---|
| Schedule | 0 3 * * * (3:00 AM UTC daily) |
| Manual trigger | workflow_dispatch enabled |
| Steps | trigger the neon-to-bq Cloud Run Job (Neon → BigQuery raw_*) → dbt run --target prod → dbt test (all BigQuery) |
| Auth | GCP via Workload Identity Federation (dbt → BigQuery, keyless); the extract-load job reads the Neon connection string from GCP Secret Manager |