A production-style batch data pipeline that ingests clickstream events, lands them in a Bronze data lake, loads them into CrateDB, transforms them with dbt into Staging/Silver/Gold layers, syncs Gold metrics to Supabase, and refreshes Metabase dashboards. The pipeline is orchestrated with Dagster and instrumented with OpenObserve for observability.
- Pulls clickstream events from an external API using a watermark-based incremental fetch.
- Writes raw events to partitioned Parquet files in Bronze storage.
- Loads raw events into CrateDB (
raw.clickstream_events). - Runs dbt models for:
- Staging cleanup and type normalization.
- Silver deduplication with incremental logic.
- Gold daily URL performance aggregations.
- Syncs Gold metrics to Supabase PostgreSQL.
- Triggers Metabase card refresh for near-real-time BI updates.
- Emits orchestration and stage-level telemetry/logs to OpenObserve.
- External FastAPI clickstream API exposes raw event data.
- Dagster schedule triggers an incremental pipeline run.
- Ingestion fetches events after the last watermark.
- Bronze writer stores raw events in partitioned Parquet.
- Raw loader inserts events into CrateDB raw table.
- dbt runs Staging, Silver, and Gold models with tests.
- Gold metrics are materialized in CrateDB analytics schema.
- Gold dataset is synced from CrateDB to Supabase.
- Metabase cards are re-queried to reflect latest data.
- Watermark is finalized for the next incremental cycle.
External FastAPI API
↓
Dagster (incremental ingestion)
↓
Bronze Layer (Parquet)
↓
dbt Tests (quality gate)
↓
Silver Layer (cleaned incremental data)
↓
Gold Layer (aggregations)
↓
CrateDB (warehouse)
↓
Supabase (serving layer)
↓
Metabase (dashboards)
Parallel observability:
Dagster logs + stage telemetry → OpenObserve
- Orchestration: Dagster
- Transformations: dbt
- Warehouse: CrateDB
- Serving Layer: Supabase PostgreSQL
- BI: Metabase
- Observability: OpenObserve (OTLP + event logs)
- Language: Python 3.11
src/
ingestion/ API fetch + normalization + watermark
lakehouse/ Bronze parquet writer
warehouse/ CrateDB load, dbt runner, Supabase sync
bi/ Metabase API integration
observability/ OpenObserve event logging + OTLP telemetry
dbt/
models/staging/
models/silver/
models/gold/
orchestration/dagster_project/
jobs/
schedules/
repository.py
workspace.yaml
data/
bronze/
checkpoints/
Assets/
batch clickstream etl pipeline.png
- Python 3.11+
- Docker Desktop
- Access to:
- CrateDB cluster
- Supabase project
- Metabase instance/API key
- Optional: OpenObserve instance
Create a local .env from .env.example and fill in values.
CRATEDB_HOSTCRATEDB_USERNAMECRATEDB_PASSWORDDBT_SCHEMASUPABASE_DB_URLSUPABASE_GOLD_SCHEMASUPABASE_GOLD_TABLEMETABASE_ENABLED=trueMETABASE_URLMETABASE_API_KEYor (METABASE_USERNAME+METABASE_PASSWORD)METABASE_CARD_IDS
OPENOBSERVE_BASE_URLOPENOBSERVE_USERNAMEOPENOBSERVE_PASSWORDOPENOBSERVE_INGEST_URLOPENOBSERVE_ORGOPENOBSERVE_STREAMZO_ROOT_USER_EMAILZO_ROOT_USER_PASSWORD
- Activate virtual environment.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned
.\venv\Scripts\Activate.ps1- Start Metabase.
docker compose up -d- Start OpenObserve.
docker compose -f docker-compose-openobserve.yml up -d- Start Dagster UI and daemon process.
cd orchestration/dagster_project
dagster devDagster loads workspace.yaml, which points to repository.py and registers:
- Job:
clickstream_pipeline_job - Schedule:
every_30_min_schedule(currently configured with*/5 * * * *, i.e. every 5 minutes)
cd dbt
dbt debug --profiles-dir . --project-dir .
dbt run --select stg_clickstream_events --profiles-dir . --project-dir .
dbt test --select stg_clickstream_events --profiles-dir . --project-dir .
dbt run --select silver_clickstream_events --profiles-dir . --project-dir .
dbt test --select silver_clickstream_events --profiles-dir . --project-dir .
dbt run --select gold_url_daily_metrics --profiles-dir . --project-dir .
dbt test --select gold_url_daily_metrics --profiles-dir . --project-dir .- Trims/normalizes text fields.
- Casts timestamps and numeric IDs.
- Standardizes
countryto uppercase.
- Incremental model.
- Deduplicates by
event_idwith latest event preference.
- Daily URL and country-level aggregation.
- Metrics:
click_countunique_visitors(distinct IP)unique_user_agentslast_event_ts
This project has automated observability at three layers:
- Schedule-level event:
orchestration_schedule:triggered
- Pipeline-level events:
pipeline_start:startpipeline_complete:success
- Stage-level events for each Dagster op:
- start/success/error with duration and contextual metadata
Signals are emitted through:
- OpenTelemetry traces/metrics to OpenObserve OTLP endpoints
- Structured event logs to OpenObserve ingest endpoint
- Bronze files:
data/bronze/dt=YYYY-MM-DD/hour=HH/*.parquet - Watermark:
data/checkpoints/watermark.json - dbt compile/run artifacts:
dbt/target/,dbt/logs/ - Dagster temp state:
orchestration/dagster_project/.tmp_dagster_home_*
Make sure runtime/generated paths are ignored:
venv/
.env
__pycache__/
*.pyc
# dbt artifacts
dbt/target/
dbt/logs/
dbt_packages/
# Dagster local runtime
orchestration/dagster_project/.tmp_dagster_home_*/
orchestration/dagster_project/.dagster/
# Local data outputs
data/bronze/
data/checkpoints/
orchestration/dagster_project/data/If you see an error about ScheduleDefinition.__init__() and an unexpected keyword, use execution_fn (not evaluation_fn) for your installed Dagster version.
Ensure these are aligned:
OPENOBSERVE_USERNAMEandOPENOBSERVE_PASSWORDZO_ROOT_USER_EMAILandZO_ROOT_USER_PASSWORD
Verify:
- CrateDB credentials in
.env DBT_SCHEMA- SSL settings and reachable
CRATEDB_HOST
Add your preferred license here (MIT/Apache-2.0/etc.).
