dbt-state-oss
An open-source, self-hosted decision server for the Apache-2.0
dbt-state client, keeping the state
store in your own storage (local disk, S3, or Azure Blob) instead of dbt
Labs' hosted, metered service.
pip install dbt-state-oss
Contents
- At a glance
- Hosting
- Why
- Behavior
- Auth
- Install & run
- State backends
- How it works
- Run the demo
- Repo layout
At a glance
pip install dbt-state-oss (add [s3] or [azure] for those backends)
State stores
| store | status |
|---|---|
local, s3, azure, gcs |
supported |
Snowflake Stage(files), Databricks Unity Catalog Volumes, Fabric OneLake Files |
roadmap |
Warehouses
| warehouse | status |
|---|---|
| postgres, snowflake | supported (tested) |
| databricks, redshift, bigquery | supported (untested - feel free to raise an issue) |
dbt engine
| engine | status |
|---|---|
dbt Core 1.7–1.11 (dbt-state plugin) |
supported |
| dbt Core 1.12+ (native, beta) | supported |
| dbt Fusion (preview) | supported |
The plugin (1.7–1.11) auto-engages once RUN_CACHE_API_URL is set. The native
engines (Core 1.12+ and Fusion) need state turned on explicitly with
DBT_ENGINE_MANAGE_STATE=true (Fusion also accepts --manage-state).
Hosting
app.state.dbt.com bundles two things: dbt Labs' cloud storage and a gRPC
decision server running 24/7. dbt-state-oss decouples them — you bring your
own storage (local / S3 / Azure) and you choose where the server runs. Two ways:
1. Co-located sidecar (simplest). Start the server next to dbt (on
localhost) just for the run; state persists in your backend between runs. No
standing infrastructure.
| host | use case |
|---|---|
| developer's local machine | local dev / interactive runs |
| GitHub Actions | CI runs (server as a background step) |
| Snowflake notebook | run alongside dbt inside Snowflake |
| Databricks notebook | run alongside dbt inside Databricks |
| Databricks dbt job / workflow | scheduled jobs (server as a sidecar task) |
2. Central always-on — your own app.state.dbt.com: one long-lived server the
whole team/CI points at (so NO-OP state is shared across runs). Add TLS + OAuth
(see Auth).
| host | fit |
|---|---|
| container / VM — Cloud Run, Azure Container Apps, ECS/Fargate, Kubernetes, plain VM | ✅ long-lived gRPC service |
| serverless — AWS Lambda, Azure Functions | ❌ request/response only, no persistent gRPC listener |
Why
dbt-state skips redundant model executions ("NO-OP" on a second run) and
auto-defers to prod, without a manifest. But the decision engine is a hosted,
metered gRPC service (api.state.dbt.com); the pip package is only a client.
With no auth, the client silently disables itself and dbt runs vanilla.
The client, the protobuf protocol, and the shared libs are all Apache-2.0. Only the server is closed. This project builds an open replacement server that:
- speaks the same gRPC protocol (reuses the client's
*Servicerstubs), - keeps all state in your own storage (local disk, S3, or Azure Blob),
- needs no dbt Labs account (insecure channel for dev; your own OAuth/Entra ID for prod).
Behavior
Verified end-to-end against our own server with zero dbt Labs:
| scenario | result |
|---|---|
| second run, nothing changed | all models NO-OP (reused, no SQL run) |
| comment / whitespace-only edit | NO-OP (semantic fingerprint) |
| real SQL change to a model | that model rebuilds |
| real change upstream | downstream rebuilds too (freshness check, cache stays safe) |
| seed file unchanged | seed NO-OP (via values_hash) |
| dev run, model not built in dev | reads its upstream from prod (defer-to-prod) |
Auth
- Dev / trusted network:
RUN_CACHE_API_URL=localhost:50051(non-:443) orRUN_CACHE_API_SECURE=false-> insecure channel, zero OAuth. In CI/non-interactive, setRUN_CACHE_OAUTH_CLIENT_SECRET=<dummy>to pass the client's disable-gate (presence-checked only; never used on an insecure channel). - Production: TLS + override
RUN_CACHE_AUTH_URL/RUN_CACHE_TOKEN_URLto your own IdP (e.g. Azure Entra ID, same identity that guards your storage). Client does OAuth2 and attaches a bearer token; the server validates the JWT.
Install & run
pip install dbt-state-oss # add [s3] or [azure] for those backends
dbt-state-oss --store local --port 50051
Then point your dbt-state client at the server (client env vars use the
RUN_CACHE_ prefix):
export RUN_CACHE_API_URL=localhost:50051 RUN_CACHE_API_SECURE=false RUN_CACHE_OAUTH_CLIENT_SECRET=dev
dbt build # in your dbt project; run twice and the second run NO-OPs
RUN_CACHE_API_SECURE=false selects an insecure channel (no OAuth);
RUN_CACHE_OAUTH_CLIENT_SECRET only needs to be present to pass the client's
enable-gate in non-interactive runs. Switch backends with --store (see the
table below), e.g. dbt-state-oss --store azure --account <acct> after az login.
State backends
Pick the backend with --store (or the STATE_STORE env var). Each backend's
config takes a CLI flag that falls back to its env var. All backends implement
the same two-method StateStore interface, so the roadmap entries are additive.
| backend | status | flags | env |
|---|---|---|---|
local |
supported | --dir |
DBTSTATE_LOCAL_DIR |
s3 |
supported | --bucket, --prefix |
DBTSTATE_S3_BUCKET, DBTSTATE_S3_PREFIX |
azure |
supported | --account, --container, --prefix |
DBTSTATE_AZURE_ACCOUNT, DBTSTATE_AZURE_CONTAINER, DBTSTATE_AZURE_PREFIX |
gcs |
supported | --bucket, --prefix |
DBTSTATE_GCS_BUCKET, DBTSTATE_GCS_PREFIX |
memory |
dev/test only | - | - |
dbt-state-oss --store s3 --bucket my-bucket
dbt-state-oss --store azure --account acct --container dbt-state
dbt-state-oss --store gcs --bucket my-bucket
dbt-state-oss --store local --dir ./.state_data
Roadmap (not yet implemented):
- Snowflake stage files, Databricks Unity Catalog volumes, Fabric OneLake files
Azure auth: DefaultAzureCredential (az login locally, OIDC/workload-identity
in CI, managed identity on Azure). The identity needs the Storage Blob Data
Contributor role on the account (control-plane Owner/Contributor is NOT enough):
az role assignment create --assignee-object-id <your-oid> --assignee-principal-type User \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<acct>
S3 auth: the boto3 default credential chain (IAM role, instance profile, SSO,
AWS_* env vars, or ~/.aws/credentials). No keys are read from this repo. The
identity needs read/write on the bucket; region comes from your standard AWS
configuration. After pip install "dbt-state-oss[s3]", start the server with
dbt-state-oss --store s3 --bucket <bucket>.
GCS auth: Application Default Credentials (gcloud ADC, GOOGLE_APPLICATION_CREDENTIALS,
workload identity, or the metadata server). No keys are read from this repo. The
identity needs read/write on the bucket; project comes from your standard Google
Cloud configuration. After pip install "dbt-state-oss[gcs]", start the server with
dbt-state-oss --store gcs --bucket <bucket>.
Next milestones: OneLake backend -> fabricspark adapter extension -> clone + prod auth.
How it works
- Client (unchanged, Apache-2.0): compiles model SQL, extracts deps + table
refs (sqlglot), reads each input's
last_modifiedfrom the warehouse via an adapter extension, hashes seed files, ships raw SQL + metadata over gRPC, acts on the verdict, and reports outcomes back. - Server (this repo): computes a semantic fingerprint, matches it against stored history for the target table, checks freshness + execution_type, and returns skip / clone / execute. Persists run records to your chosen backend (local, S3, or Azure Blob).
Our fingerprint algorithm only has to be self-consistent between "record a run" and "check a run" - it does not need to match dbt Labs'.
Run the demo
A runnable seed -> staging -> mart project that NO-OPs on the second run lives in
example_project/. It ships only in the repo (not the pip package) and needs a
postgres with track_commit_timestamp=on — the client reads freshness from
pg_xact_commit_timestamp. The example profile expects postgres on :5433,
database dbt_oss. Clone the repo, install with the dev extra, start the
server (--store local), then dbt build --target prod twice from
example_project/.
Repo layout
(The pip package ships only dbt_state_oss/; the rest is for development.)
dbt_state_oss/ the gRPC decision server (the engine)
example_project/ a tiny dbt-postgres project (seed -> staging -> mart) for local testing
tests/ unit + S3 integration tests
docs/ PROTOCOL.md (the reverse-engineered contract), FINDINGS.md (the eval)
reference/ local copy of dbt-labs' Apache-2.0 client source (gitignored, not committed)
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dbt_state_oss-0.1.1.tar.gz.
File metadata
- Download URL: dbt_state_oss-0.1.1.tar.gz
- Upload date:
- Size: 23.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
28c67cb73214e376b12c6dc478bc326eaa7c7b90a4d60f5b43e9a491de284b19
|
|
| MD5 |
3a503fe96974bb84548463fa26beecc3
|
|
| BLAKE2b-256 |
3b2b2094aad0ef23a9c3bfbc8bf3cc478da0fce1453f3a5c89850e423f1d8261
|
File details
Details for the file dbt_state_oss-0.1.1-py3-none-any.whl.
File metadata
- Download URL: dbt_state_oss-0.1.1-py3-none-any.whl
- Upload date:
- Size: 18.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ddca9058195df5e5491a2da673cd889e78aac29714e2868620d7f22e05aa2b13
|
|
| MD5 |
c7b7b4ffd553cca8af42d60c734be472
|
|
| BLAKE2b-256 |
c40500b678137e812fa3adf7952133fa9f90b13fe578cb24bf98d65cda6ecfc8
|