Skip to main content
Locus’s analytics tables are built by a dbt-core project (axiom_locus) that transforms raw ingestion tables into typed, tested, APRS-compliant marts. Marts are the layer downstream consumers — the dashboard heatmap, Explorer, scoring service, and API — read from. Staging and intermediate models are internal scaffolding; only marts are part of the public contract.
Locus uses dbt-core only. There is no dbt Cloud deployment. Local development materializes to DuckDB; production runs against the Locus Postgres database.

Project layers

The axiom_locus dbt project has three model layers with fixed materializations:
LayerMaterializationPurpose
stagingviewThin, renamed views over raw source tables (building_permits, nasa_firms_active_fires, cell_scores). One staging model per source.
intermediateephemeralReusable joins and rollups (e.g. int_safety_signals_per_cell). Compiled inline — never persisted.
martsincrementalAnalytics-ready tables. Incremental by ingested_at watermark with on_schema_change='append_new_columns'.
Staging models normalize column names, cast types, and pass through the APRS envelope. Marts apply the aprs_envelope macro so every mart row carries record_id, source_uri, source_system, schema_version, normalization_version, acl_tier, occurred_at, ingested_at, and modified_at.

Sources

The dbt project reads from three source tables in the public schema:
SourceIdentifierOwnerdbt usage
permitsbuilding_permitsMunicipal permit collectorsRead-only via stg_permits
nasa_firmsnasa_firms_active_firesNASA FIRMS ingestionRead-only via stg_nasa_firms
cell_scorescell_scoresLocus scorer serviceRead-only via stg_cell_scores
cell_scores is owned by the scorer service. dbt reads it through stg_cell_scores but never writes to it. Do not point dbt models at cell_scores as a materialization target.

mart_axiom_events

mart_axiom_events is the unified event surface for Locus. It is the same axiom_events signal the dashboard heatmap and Explorer read from. Each row is one real-world event with H3-cell attribution and full APRS envelope.

Configuration

{{ config(
    materialized='incremental',
    unique_key='event_id',
    on_schema_change='append_new_columns'
) }}
  • Incremental — only rows where ingested_at is newer than the current watermark are added on each run.
  • Append-onlyevent_id (an md5 of {source}:{record_id}) is unique and stable. Re-running dbt is idempotent.
  • Schema-evolution-safe — new columns added to the model are appended to the table without rebuilds.

Columns

ColumnTypeDescription
event_idtextMD5 URN of {source}:{record_id}. Unique, stable across runs.
entity_idtextLogical entity the event applies to (e.g. permit:123, h3:8828308281fffff).
entity_typetextpermit, h3_cell, etc.
entity_nametextHuman-readable label.
event_typetextpermit.filed, permit.approved, permit.denied, permit.completed, fire.detected.
event_categorytextdevelopment or safety_environment.
h3_indextextH3 r8 cell ID. Always populated (rows without h3_index are filtered upstream).
magnitudedouble precisionDomain-specific magnitude: permit estimated cost, fire radiative power (FRP).
confidencedouble precision0.0–1.0 confidence in the event. Permit events use 0.70; FIRMS detections map H/N/L to 0.90/0.70/0.45.
APRS envelopevariousrecord_id, source_uri, source_system, schema_version, normalization_version, acl_tier, occurred_at, ingested_at, modified_at (see APRS data standard).
metadatajsonSource-specific extras, e.g. {"source":"building_permits"}.

Current event sources

Part A ships with two unioned sources:
Sourceevent_type valuesevent_category
building_permitspermit.filed, permit.approved, permit.denied, permit.completeddevelopment
nasa_firms_active_firesfire.detectedsafety_environment
Additional event sources land in Part B without changing the row shape — on_schema_change='append_new_columns' lets new columns appear without a rebuild.

Querying

mart_axiom_events is a regular Postgres table — query it with any SQL client:
-- Fire detections in the last 7 days, ranked by radiative power
select event_id, h3_index, magnitude, confidence, occurred_at
from mart_axiom_events
where event_type = 'fire.detected'
  and occurred_at > now() - interval '7 days'
order by magnitude desc
limit 50;
Join to cell_scores to pull current scoring context for any event’s cell:
select e.event_id,
       e.event_type,
       e.occurred_at,
       cs.composite_score,
       cs.confidence as score_confidence
from mart_axiom_events e
join cell_scores cs using (h3_index)
where e.event_category = 'development'
  and e.ingested_at > now() - interval '24 hours';

The aprs_envelope macro

Every mart row carries the APRS envelope via a single macro call, so envelope shape is identical across sources:
{{ aprs_envelope(
    source_system="'municipal_permits'",
    source_uri="'public.building_permits'",
    record_id_expr="'urn:aprs:record:locus:permit:' || coalesce(permit_id, source_id, 'unknown')",
    occurred_at_expr="coalesce(cast(issue_date as timestamp), ingested_at)",
    ingested_at_expr="ingested_at",
    modified_at_expr="modified_at",
    acl_tier="'research'"
) }}
Defaults: schema_version='aprs.locus/1.0.0', normalization_version='1.0.0', acl_tier='research'. Override per source when needed.

Local development

Install the dbt adapters into a Python environment:
pip install dbt-duckdb dbt-postgres
Then build the project against DuckDB:
cd packages/dbt-locus
dbt deps
dbt build --profiles-dir profiles --target dev
The dev target writes to /tmp/axiom_locus.duckdb. Use any DuckDB client to inspect compiled marts locally.

Targets

The project ships three profiles:
TargetAdapterWhere it writes
devduckdbLocal /tmp/axiom_locus.duckdb. Used for iterative development.
cipostgresPull-request CI environment. Reads DBT_POSTGRES_HOST, DBT_POSTGRES_USER, DBT_POSTGRES_PASSWORD, DBT_POSTGRES_DB, DBT_POSTGRES_PORT.
prodpostgresLocus production Postgres. Same env vars as ci.
Both Postgres targets materialize into the public schema by default.

Continuous integration

A GitHub Actions workflow runs dbt deps && dbt build --profiles-dir profiles --target ci on every pull request that touches the dbt project or the workflow file itself. The build executes every staging, intermediate, and mart model plus the not_null / unique schema tests defined in models/marts/schema.yml. PRs that break a model or fail a test are blocked at merge. Schema tests currently enforced on mart_axiom_events:
  • event_idnot_null, unique
  • occurred_atnot_null
  • h3_indexnot_null

Roadmap

Three marts are planned. Part A ships mart_axiom_events; the remaining two land in Part B without changes to the existing mart:
  • mart_cell_score_composite — replaces the in-app composite computation in the scorer service.
  • mart_metro_overview — replaces the hand-rolled metro overview SQL in the web service and retires the legacy refresh_metro_overview pg_cron job.
The full retirement plan for legacy pg_cron event-source jobs runs in stages: land marts, parity-check against the existing pg_cron-derived tables, migrate downstream consumers, then disable the pg_cron jobs.