Skip to main content
Canonical reference: This guide describes the extensibility contract as it is actually implemented in the repository. Read it in full before creating a single file.

Eight traps, each one measured

Every item below is a place where an earlier version of this guide described something the repository does not do. Each carries the command that re-establishes the fact and the gate that catches it — or the honest admission that nothing does. There are six conformance layers, not four (see the table at the end of this page).

Overview

What You Get for Free (The Shared Base)

Any connector that respects the contract inherits the following mechanisms without writing a single line in server/core/:

What You Must Write (The Cost of a New Connector)

No file inside server/core/ may be modified: a connector is added by its own folder and its manifest, never by editing the platform. A new daily connector is registered in two places, and skipping the second one produces rows nothing reads:
  1. dbt/dbt_project.yml → model-paths: add ../server/modules/<name>/dbt, then dbt parse.
  2. dbt/models/marts/fact_daily_kpi.sql: add the connector’s stg_<name>_daily to the UNION.

Choosing the Landing Kind

The module_kind field in manifest.json declares the structure of rows produced by the module:

Walkthrough: read a shipped connector, then copy its shape

The fastest way to write a connector is to open one that works. server/modules/brevo/ is complete and passes every conformance layer — manifest 1.2, connector.py, dbt staging model, seed loader, fixtures, generated catalog. Every excerpt below is copied from it.
server/modules/README.md carries the Connector Standard Playbook — the exhaustive field catalog, the doc↔code contract, account topology and the error taxonomy. Read it alongside this page: it is what separates a connector that runs from one that can be operated. server/modules/meta-ads/ is its most complete implementation.

Step 1 — What lives on disk

No file under server/core/ may be touched.

Step 2 — The manifest is the contract

The header declares who you are and how you authenticate:
source_capabilities.reports[] declares what can actually be asked for — the metrics, the dimensions, the supported grains, and the function the worker will call:
canonical_metric_mapping translates the provider’s vocabulary into the platform’s, so the same figure is comparable across sources:
The worker resolves source_capabilities.reports[].dispatch.callable and fails closed if that function does not exist. A connector can be installed, cataloged and green while no pull ever leaves — this is trap 2 above, and test_pull_contract.py is what catches it.

Step 3 — connector.py: a closed signature

Those five positional parameters are the whole contract. Anything a connector needs beyond them — a reporting account, a site URL — arrives as an optional keyword argument whose name is declared in account_topology.pull_parameter. Make it required and the first real call dies with TypeError, which is exactly what happened to gsc on 2026-07-31. Two more rules the shared base enforces:
  • land rows through core.warehouse_write.open_raw_writer, never a bare duckdb.connect — a bare connection writes into everyone’s raw zone;
  • raise through classify_http_error(status, body, error_map) from core/pull_errors.py. A bare RuntimeError on a non-200 means the worker cannot tell an expired token from a bad request: no reconnection offered, no breaker, no evidence kept.

Step 4 — The staging model deduplicates by pull

Re-running an extraction must not double a figure. The staging model keeps the newest pull per grain, and that is the whole of it:
dbt/staging/schema.yml carries the grain_unique test that fails if that partition ever lets a duplicate through.

Step 5 — Register in two places

Skipping the second produces rows nothing reads:
  1. dbt/dbt_project.yml → model-paths: add ../server/modules/<name>/dbt, then dbt parse.
  2. dbt/models/marts/fact_daily_kpi.sql: add stg_<name>_daily to the UNION.

Step 6 — Prove it offline

seeds/load_<name>_seed.py loads a fixture into the local warehouse so the staging model runs without a single API call. Without it, dbt run breaks on your model and the connector counts as covered while its warehouse half was never exercised.
tests/fixtures/golden_pull.json and expected_facts.json are the recorded provider response and the facts it must produce. The conformance suite replays one into the other, so a change in your transform that alters a figure fails before it reaches a warehouse.

Final Conformance Checklist

  • manifest.json schema version 1.2 with source_capabilities (conformance_layer_1 pass).
  • source_capabilities.reports[].dispatch.callable names a function that EXISTS (test_pull_contract).
  • pull() keeps the closed signature; every reachable return carries pull_id, row_count, date_from, date_to.
  • transform() is manifest-driven (no hardcoded field names).
  • Rows land through core.raw_landing / warehouse_write — no bare duckdb.connect.
  • Non-200 raises through classify_http_error + error_map — no bare RuntimeError.
  • No account in an environment variable, and none as a required argument.
  • Google connector: manifest auth block with its scope (test_google_consent.py green).
  • QUALIFY supersede staging model with the mandatory stg_<name>_daily_grain_unique test.
  • Registered in dbt_project.yml → model-paths and in the fact_daily_kpi UNION.
  • Seed loader written and run; dbt build --select stg_<name>_daily green.
  • Fixture files golden_pull.json and expected_facts.json verified (conformance_layer_4 pass).
  • All six conformance layers pass cleanly before merging.

Next Steps & Cross-References

Universal Datastreams

Learn how datastreams manage ingestion, mappings, and versioning.

Semantic Layer

Understand canonical fact surfaces and dbt semantic ratio views.

Platform Constraints

Review multi-tenant isolation rules and security invariants.

Quickstart Guide

Test your new connector in the local developer sandbox.