Loading documentation...
Definitions for core concepts, architecture, and terminology used across the HeatWaves application.
This page defines how words are used in this repository (especially under docs/ingestion and docs/fire-data-schema). It clarifies concepts significant to understanding how the HeatWaves application works, mapping the complex landscape of fire statistics to our internal architecture.
Authority and boundaries:
category-values-by-source.md.taxonomy-and-eu-firestat-alignment.md and eu-firestat-final-report.md.postgres-contract-rls-and-keys.md and pipeline-slices-idempotency-and-triggers.md.HeatWaves
The HeatWaves app is built to make sense of a confusing, complex, and fragmented landscape of fire statistics. It consolidates data from every publicly available database for fire statistics in Norway, presenting this data intuitively as "waves" (visual representations of consolidated terminologies over time, typically displayed as line charts) to allow users to see trends across multiple disparate data sources simultaneously.
Standard Code (standard_code)
One of the major features of the app. To preserve transparency, the app maintains the original labels from the sources, but maps them to standard codes that work across all data sources and their dimensions. For example, if you want to see "Electrical" fires, the standard_code consolidates "Electrical causes" from Police, "Fixed and loose electrical" from DSB, etc. into a single unified view.
Native Code / Native Name
The original, unmodified vocabulary and labels from the upstream sources. The app stores and displays these native labels to preserve the fidelity and transparency of the original data, while using standard codes behind the scenes to bridge the differences. Facts still store integer FKs, not raw strings on fact columns.
Database
When we refer to "our Database", we mean our Supabase PostgreSQL instance, specifically the fire_data schema where normalized facts and metadata are stored. Other "upstream databases" refer to the external systems we pull data from, namely BRASK and BRIS.
Datasource (Data source)
A logical provider of data in HeatWaves, represented by a source_id. While BRIS is a single upstream database, HeatWaves distinguishes between the different entities reporting into it. Thus, Police, Fire Brigade, and DSB are treated as distinct "Data sources" within the app to accurately reflect their different datasets and filter capabilities. Other sources include BRASK and SSB.
BRIS / BRASK / SSB
Norwegian fire-statistics upstreams wired by adapters (police/brigade/DSB APIs, Finans Norge BRASK form, Statistics Norway PxWebApi / json-stat2). Domain semantics: taxonomy-and-eu-firestat-alignment.md and category-native-to-cat-code.md.
PxWebApi / json-stat2
SSB API shapes for table 12058 and related queries. See ssb-pxwebapi-tables.md.
Fire Characteristics (EU Firestat)
The fundamental concepts of a fire incident, aligned with the EU Firestat framework: Primary Causal Factor (Fire cause), Area of Origin, Heat Source, and Item First Ignited / Object.
Dimensions
How the Fire Characteristics are instantiated and stored in the HeatWaves app. Different data sources use different naming conventions for these concepts (e.g., DSB calls Area of Origin "Antatt arnested", while Police calls it "Arnested", and others ask "Hva brannen startet i?"). In HeatWaves, these are unified under a single Breakdown dimension (dim_code) so the UI can show comparable bars/series regardless of the native upstream name.
Adapter
TypeScript module under lib/data-model/adapters/ that fetches a native API (BRIS, BRASK, SSB), transforms responses into typed fact rows, and upserts into fire_data. Wired through ingestBySourceId in _dispatch.ts. See typescript-entrypoints-dispatcher-and-registry.md.
Atom (atomic row)
A single fact row at the finest stored grain: one natural-key combination, one count (and optional monetary metric). Cross-source roll-ups such as RESIDENTIAL exist in registry metadata (filters.standard_code) and chart/RPC resolution — they are not stored as extra “total” rows in fact_*. The per-axis «Alle» / total is the NULL-FK slice, never a stored roll-up filter row. See pipeline-slices-idempotency-and-triggers.md §5.2–5.3.
Axis (filter axis)
Named filter domain on facts: fire_type, building_type, building_age, region. Rows reference filters.id for the axis; filters.axis must match. Applicability to a breakdown dimension is constrained by filters.applicable_dim_codes and enforced by triggers. See conceptual-tables-and-grain.md §“Filter axes”.
Breakdown dimension
Analytical axis shown as bars/series (dim_code + per-row cat_code on facts). Distinct from filter axes, which narrow all series uniformly. Decision rule: “Does the UI show bars per value?” → dimension. See conceptual-tables-and-grain.md §“Three-bucket framework”.
Bucket (upstream)
Key/value map in a native API response (e.g. BRIS buckets keyed by Norwegian labels, or json-stat dimension indices). Some keys are sentinel tokens (special markers, not human labels). Adapters resolve bucket keys to categories / filters via the registry; missing category rows can drop counts silently relative to totals. See catalog-governance-and-reconciliation.md §1 and Sentinel (token) below.
cat_code
Integer category code scoped to (source_id, dim_code) in fire_data.categories. Part of the fact natural key and the within-slice breakdown axis. Never shared across sources. See category-native-to-cat-code.md.
Catalog
The seeded lookup tables (sources, dimensions, categories, filters) that define allowed vocabulary and mappings. Loaded once per server request via getCatalog() (cached; passed to the client console as a prop). Governance of completeness is operational policy; physical constraints are FKs + triggers. See catalog-governance-and-reconciliation.md and the read-path doc for the TS API.
Coverage (slice coverage)
Whether the database already has usable fact rows for a given slice + window so the app can chart without running ETL. Explored via probe RPCs and year/date range checks; /explore branches to charts, partial heal, cold ingest (signed-in), or a login prompt (anonymous). See explore-fire-data-slice-probe-and-ingest.md.
Crosstab
BRASK-style HTML table layout where chosen row/column dimensions form a grid of numeric cells. Ingest interprets cells as facts at the configured grain. See crosstab-and-bucket-modeling.md.
dim_code
Integer FK to fire_data.dimensions identifying the breakdown dimension (e.g. 1 Årsak, 2 Tennkilde, 3 Objekt, 4 Arnested — confirm live seed). See category-native-to-cat-code.md.
Dispatcher
ingestBySourceId in _dispatch.ts: selects the adapter for a source_id, loads SourceRegistry once per run, iterates slices. See typescript-entrypoints-dispatcher-and-registry.md.
enforce_fact_filter_code_axes()
Postgres function invoked by BEFORE INSERT OR UPDATE triggers on fact_yearly / fact_daily. Rejects rows whose non-null filters.id references the wrong filters.axis or violates filters.applicable_dim_codes for the row’s dim_code. See pipeline-slices-idempotency-and-triggers.md §1.
ETL
Extract–transform–load: in this project, the path from native APIs → adapters → fire_data fact tables. Chart reads are separate (RPC layer). The main narrative doc is pipeline-slices-idempotency-and-triggers.md.
Explore / Utforsk
The /explore workspace: probes coverage, may trigger POST /api/admin/ingest, polls ingest_runs, then reads charts via get_fire_data. See explore-fire-data-slice-probe-and-ingest.md.
fact_daily / fact_yearly
Physical fact tables at date vs calendar year grain. Shared filter spine (fire_type_code, …); time column is date vs year. Each has an 8-column natural key (seven semantic keys + time). See conceptual-tables-and-grain.md.
fire_data schema
Postgres schema holding analytical tables, RLS policies, and RPCs exposed to the app (via Supabase/PostgREST). Contrasts with the hidden ingest schema used for operational extraction bookkeeping. See conceptual-tables-and-grain.md §“Two schemas”.
Fan-out
Issuing many upstream requests for one logical slice (e.g. BRASK daily: one POST per (year, month)). See pipeline-slices-idempotency-and-triggers.md §10.3.
FireDataQuery
App-layer query intent (sources, dimension, facets, time window, granularity) before calling Postgres. Built inline on Tier-2 routes (/simple, /atlas) or via the Explore adapter from SliceParams. Synced to RPC parameters by resolveFireDataRequest. See read-path-get-fire-data-and-catalog-caching.md.
Grain / granularity
Time resolution of stored and read facts: year uses fact_yearly; day · week · month · quarter use fact_daily for sources with supports_subyear = true. get_fire_data does not derive yearly totals by scanning fact_daily. See incremental-patches-and-rpc-changelog.md and pipeline-slices-idempotency-and-triggers.md §10.
get_fire_data
Primary RPC for chart aggregates. Accepts date range, source/dimension ids, optional TEXT[] filter tokens (native_code or standard_code), granularity, and metric. See incremental-patches-and-rpc-changelog.md §“RPC note”.
getCachedFireData
TypeScript wrapper that calls the get_fire_data RPC with Next.js 'use cache' and tag-based invalidation (fact:*, fact:all:all). All canon chart routes use this — not the uncached getFireData helper (internal to ingest/scripts only). Same GetFireDataArgs in, same GetFireDataRow[] out. See read-path-get-fire-data-and-catalog-caching.md.
Idempotency (ingest)
Safe to re-run: same logical input converges on the same rows via upsert on the natural key. Two-tier behaviour compares upstream grand totals (and sometimes per-year/per-month maps) to DB sums to skip work when unchanged. See pipeline-slices-idempotency-and-triggers.md §5.
Ingest / ingestion
The act of loading or refreshing facts from upstream into fire_data, typically via POST /api/admin/ingest → ingestBySourceId → adapter upserts. Distinct from probe (read-only coverage check).
ingest_runs
fire_data.ingest_runs: orchestration row per slice run (run_id, status, counters, optional phase_detail, generated slice_hash, …). Used for Explore polling and operator visibility. See explore-fire-data-slice-probe-and-ingest.md.
Ledger (ingest undo)
Mechanism tied to undo_ingest_run and written keys for reverting a completed run; natural keys distinguish fact_daily vs fact_yearly. See Explore architecture doc §“Undo path”.
Metric
What is aggregated: e.g. count (value) vs monetary kr (value_kr; BRASK kr scale is documented in the pipeline doc). Selected via p_metric on get_fire_data and slice descriptors for BRASK.
Natural key
Unique business key for a fact row. Yearly: (source_id, dim_code, cat_code, year, fire_type_code, building_type_code, region_code, building_age_code) with NULLS NOT DISTINCT so all-null filters do not multiply rows. Daily: same with date replacing year. See postgres-contract-rls-and-keys.md.
Outbound / inbound translation
Outbound: integer slice + registry → native HTTP request fields. Inbound: native response keys → cat_code / filters.id. Both use the same SourceRegistry. See pipeline-slices-idempotency-and-triggers.md §2.1.
Phase detail
JSON progress payload on ingest_runs for long runs (e.g. daily four-tier adapters). Consumers use canonical_progress when kind is brask_daily_fanout or bris_police_daily_fanout. See pipeline-slices-idempotency-and-triggers.md §10.3–§10.4.
PostgREST
Supabase’s auto-generated REST API over Postgres. The app uses .schema('fire_data') for allowed tables/RPCs. Adapters use the service role for writes.
Prefetch (fact prefetch RPC)
Database helpers such as fire_data.fact_yearly_slice_ingest_prefetch / fact_daily_slice_ingest_prefetch returning aggregates (grand_total, by_year, …) without shipping all fact rows — used for idempotency gates and daily tiers (BRASK + BRIS Police). See pipeline-slices-idempotency-and-triggers.md §5.5–5.6, §10.3–§10.4.
Probe
Small read-only RPC (fire_data.probe_slice_coverage, probe_slice_coverage_daily) answering: does this slice have chartable rows in facts, and has a run for this slice_hash completed? Not full chart SQL and not ETL. See explore-fire-data-slice-probe-and-ingest.md.
RLS (row-level security)
Postgres policies on fire_data enabling anonymous read of published aggregates while keeping writes service-role-only. Contract: postgres-contract-rls-and-keys.md.
RPC (in this codebase)
A fire_data stored function (CREATE FUNCTION …) called through Supabase as .rpc('name', { … }). Analytical reads go through RPCs (e.g. get_fire_data, probes, catalog helpers), not ad-hoc SQL strings from the client. Templates and security notes: conceptual-tables-and-grain.md §“RPCs, never raw SQL”.
Sentinel (token)
A special bucket key in an upstream API map that stands for a fixed meaning rather than a native category label. General programming term: a sentinel value marks an exceptional case (missing data, end-of-list, etc.) distinct from normal values. In HeatWaves ingest, the main example is BRIS aggregate '-1': missions where the breakdown question was not answered. Adapters translate that sentinel to catalog native_name = 'Ikke besvart' (resolveCatCodeFromBrisBucket / BRIS_MISSING_ANSWER_BUCKET in _registry.ts), then to the reserved cat_code slot (x99, standard_code = UNKNOWN). Without that catalog row, -1 counts are dropped and BRIS slice totals fail reconciliation. See category-native-to-cat-code.md §5 and pipeline-slices-idempotency-and-triggers.md §4.1.
slice_hash
Deterministic fingerprint of canonical slice JSON on ingest_runs, GENERATED ALWAYS in Postgres from fire_data.compute_slice_hash(slice_config). Dedupes concurrent Explore triggers via partial unique index on active statuses. See explore-fire-data-slice-probe-and-ingest.md.
Slice
Minimal integer-id descriptor of what to fetch from upstream for one adapter pass: source_id, dim_code, optional filter codes (fire_type_code, …), year or date window, and source-specific options (e.g. BRASK metric). Drives outbound request building and idempotency key scope. See pipeline-slices-idempotency-and-triggers.md §2.2 worked example.
SourceRegistry
In-memory maps loaded once per ingest run (loadSourceRegistry): native ↔ cat_code / filters.id lookups and outbound id→native. See pipeline-slices-idempotency-and-triggers.md §3.
Tier-0 / Tier-1 / … (daily ingest)
Sequential gates in brask.daily.ts (BRASK) and bris.daily.ts (BRIS Police): cheap DB checks before issuing the expensive upstream call(s). Distinct from the Explore coverage probe (different layer). See pipeline-slices-idempotency-and-triggers.md §10.3–§10.4.
Two-tier idempotency
Tier 1: compare upstream grand total vs SUM(value) for slice keys → skip upsert if equal (and DB non-zero). Tier 2: if totals differ, diff per-year (or per-month/day) aggregates and upsert only changed buckets. See pipeline-slices-idempotency-and-triggers.md §5.
Upsert
INSERT … ON CONFLICT … DO UPDATE on the natural key; batch variants upsert_fact_yearly_batch / upsert_fact_daily_batch may skip writes when IS DISTINCT FROM detects no change.
Within-slice axis
Dimensions that vary inside one adapter response while slice keys stay fixed — e.g. cat_code and time (year / date) for category breakdowns. Slice keys fix the “outer” filter tuple + source + dim. See pipeline-slices-idempotency-and-triggers.md §5.1.
When to update this file:
category-values-by-source.md and taxonomy-and-eu-firestat-alignment.md.