Configuration Reference¶
Every setting Paridity resolves at run time, where it lives, how conflicting values are combined, and what it changes about a reconciliation.
How to read this document. Paridity does not have one settings screen. A value can be declared at up to seven levels, and the platform combines them by an explicit rule per setting — usually "the most specific wins", but sometimes "the tightest wins" or "all must agree". If you only read one section, read The seven layers and the Reachability legend.
Source of truth. The tables below are rendered from
src/services/config_resolver.pyand the migration chain indb/migrations/, not from a design spec. Where a spec and the code disagreed, the code won and the spec was corrected (2026-08-01).The "Default (fresh install)" cell of every L1 row is generated by
scripts/gen_config_reference_defaults.pyand gated in CI: it is the value thecfg.platform_defaultsrow holds on a freshly migrated database (#3106). Your deployment can differ - an operator may have changed the row since (the development stack's demo data, for example, raisesegress_row_limit) - and Settings → Platform & Tenant Defaults always shows the live value.
The seven layers¶
Configuration resolves from seven layers. For most settings the most specific layer that defines a value wins; the exceptions are listed in Resolution rules and flagged on every affected row.
| Layer | Where it lives | Scope | Who sets it |
|---|---|---|---|
| L1 Platform defaults | cfg.platform_defaults (one row) |
The whole installation | Your Paridity operator, at deployment. Visible read-only in Settings → Platform & Tenant Defaults |
| L2 Tenant defaults | cfg.tenant_defaults |
One tenant | Platform admin, in Settings → Platform & Tenant Defaults. Canonicalization defaults are visible there but operator-managed |
| L3 System profile | cfg.systems |
One source or target system | Platform admin, in Systems |
| L4 Mapping | cfg.mappings |
One mapping | Operator, in Mappings → Advanced |
| L5 Field override | cfg.mapping_fields |
One column of one mapping | Operator, in Field Config |
| L6 Schedule | cfg.schedules |
One schedule of one mapping | Operator, in Schedules |
| L7 Run override | execution_requests.run_overrides |
One run | platform_admin, at run submission |
A run is pinned to the configuration that existed when it started. Resolution
produces an immutable snapshot stored in cfg.config_versions, and the run
references that snapshot by id. Editing a mapping mid-run never changes the run
in flight, and the snapshot's config_checksum is what proves to an auditor
which policy was in force.
Reachability legend¶
Every stored value is visible in the product. What varies is where you can change it. Each option below carries one of these markers.
| Marker | Meaning |
|---|---|
| UI | Settable in the web application. |
| API | Settable through the REST API (and therefore automatable), but with no dedicated screen. |
| Ops | Visible in Settings → Platform & Tenant Defaults, but changed by your Paridity operator with direct SQL rather than from a form. (The platform row and its defaults come from the database migrations; db/seeds/platform_defaults.sql sets no L1 value.) |
| Derived | Not set directly — computed from other settings or introspected from the engine. |
Why "Ops" exists — and why it no longer means "hidden". Every L1 and L2 value is listed in Settings → Platform & Tenant Defaults, grouped, with the layer it comes from. An auditor or evaluator can therefore read the platform's entire comparison policy — including exactly what it treats as equal — without database access.
A subset stays operator-managed rather than editable in a form. That is deliberate, and there are two reasons:
- Canonicalization defaults (
null_token,numeric_scale,timestamp_precision,timezone,trim_strings,null_equals_empty,hash_spec_version) define what "equal" means. Changing one mid-stream silently alters every future comparison and makes prior evidence no longer directly comparable — there is no re-baseline step that would make that safe as a settings toggle. The screen shows each one with that explanation.- Signing key references point into your secrets backend and belong to the deployment, not the application. (The evidence key pointer,
evidence_kek_ref, is the exception since #2395: it is editable in-app by a platform admin, because a tenant that cannot set it has masked-evidence reveal permanently off.)Everything else at L2 — capacity, readiness, diff-safety limits, retention floors, evidence privacy, and the comparison-methodology flags — is editable on that screen by a platform admin, and every change is written to the audit log. L1 is read-only throughout: it is the floor a tenant inherits when it sets nothing.
Resolution rules¶
Most settings are "lowest layer wins". These are the exceptions — each is a deliberate safety property, not an inconsistency.
| Setting(s) | Rule | Why |
|---|---|---|
max_concurrency |
MIN across L1, L2, L3, L4, L6 | A system's own limit is a hard ceiling — a mapping can never ask a database for more parallelism than that database was declared to tolerate. |
max_partition_concurrency |
MIN across L4, L1 default, and both systems' L3 max_concurrency |
Same ceiling logic, applied to parallel partitions. |
egress_row_limit, per_column_diff_limit, global_diff_cell_limit |
MIN across L1, L2, L4, L6 — and an L7 run override may only tighten | Cost and blast-radius caps must not be raisable by a single run. |
store_raw_diff_values |
AND across L2, L4, L5 — any FALSE suppresses |
A privacy suppression at any level must be final. Values are stored as NULL with values_masked = TRUE. |
retain_*_days |
L4 wins, else the L1 default; cannot go below the L2 minimum | A mapping can retain evidence longer than the tenant floor, never shorter. |
priority |
MAX across L1 and L6 | A platform floor can be raised for an urgent schedule and never dropped below. |
cdc_integration_mode |
L4 only | Changing how CDC signals gate a comparison is a policy change; a schedule or a single run must not be able to weaken it. |
cdc_table_semantics |
L4 only | Declares how the table's commit point relates to the one the comparison reads (#2853); a schedule or a single run must not be able to switch the hold off. |
read_pin_views |
L4 only | Declares that the mapping's Snowflake view(s) may be pinned at the readiness instant (#2933); what a run reads must not vary by schedule or run. |
fail_fast_threshold_pct |
Lowest wins; an L7 override may lower it | — |
diff_truncation_method |
L1 → L2 → L4, L4 wins; L6/L7 may select only first_n or random_sample |
stratified has resource implications, so it stays a mapping-level decision. |
mask_in_ui |
L5 (field) only | A tenant-wide "mask everything" policy is not implemented — do not assume one exists. |
Comparison semantics (canonicalization)¶
These decide what "equal" means. They are the highest-consequence settings in the product: changing one changes which rows are reported as differences.
| Option | Layers | Default (fresh install) | Values | What it changes |
|---|---|---|---|---|
null_token |
L1, L2 — Ops | <NULL> |
any string | The literal a NULL becomes before hashing. Both sides must agree or every NULL-bearing row mismatches. |
trim_strings |
L1, L2, L4 — Ops/API | true |
bool | Strips the six ASCII whitespace characters (space, tab, newline, vertical tab, form feed, carriage return) — not Python's 29 Unicode whitespace codepoints. Non-breaking space and U+3000 are deliberately kept. |
null_equals_empty |
L1, L2, L4 — Ops/API | false |
bool | When true, an empty string (after trimming) is treated as NULL. Turn this on only when one side genuinely stores '' for absent data. |
numeric_scale |
L1, L2 — Ops | 6 |
1–38 | Decimal places every numeric is quantized to before hashing. Too low silently equates genuinely different values; too high can surface float noise as differences. Per-field overrides are rejected — this is global by design. |
timestamp_precision |
L1, L2 — Ops | millis |
seconds, millis, micros |
Truncation applied to every timestamp. Set this to the coarsest precision your two systems agree on. |
timezone |
L1, L2 — Ops (DB-only: the config-defaults API lists it read-only) | UTC |
IANA name | How naive timestamps are interpreted. Non-UTC values change which rows fall inside an incremental window, not just how they render — and, for a time partition strategy, which rows each partition holds: a partition day is a day in this timezone, rendered per side by the column's comparison frame (naive columns compare wall-clock digits, timezone-aware columns compare instants). There is no per-mapping timezone; a change takes effect on the next config version each mapping mints. |
numeric_tolerance |
L4 only — API | none | decimal | Absolute difference below which two numerics are considered equal. Use for known float drift; it weakens exactness, so prefer fixing the source. |
hash_spec_version |
L1 — Ops | 1.0 |
— | The canonicalization contract version. Changing it changes every hash. |
Both sides always use the same rules. Canonicalization is resolved once per mapping and applied identically to source and target. That is what makes a cross-engine comparison meaningful.
SCD scope (SCD2 / bitemporal tables)¶
Scopes the comparison to the current rows of a side that legitimately keeps closed history rows. Mapping-level only, always operator-declared — the platform never infers SCD2-ness. See Mappings — declared scope.
| Option | Layers | Default | Reach | What it changes |
|---|---|---|---|---|
scd_scope |
L4 only | none | UI | The whole declaration: scoped side, mode (current_only is the only v1 mode), a typed current_predicate (flag_equals, end_ts_sentinel, or end_ts_null — columns and values, never SQL), optional interval_columns, and assertions. Stored in the checksummed config snapshot, so every run records the exact predicate it was scoped by. |
assertions.unique_current_per_key |
— | true |
UI (fixed) | The I1 invariant — always checked when a scope is declared; not disableable. |
assertions.no_interval_overlap |
— | false |
UI | The I2 invariant — opt-in (extra self-join per run), requires interval_columns. |
Rules the reference should not let you discover the hard way: the scoped
side's engine must be live-verified for scope rendering (currently
postgres, snowflake — anything else is rejected, fail-closed); scope
columns may not also be key or mapped comparison columns; key_columns must
be declared; and the scope composes with (never replaces) the side's
filter_sql. Watermark queries are never scoped by scd_scope — see
Watermarks.
Per-field overrides (L5)¶
Set in Field Config. All are UI-reachable unless noted.
| Option | Values | What it changes |
|---|---|---|
exclude_from_hash |
bool | Column is omitted from the row fingerprint — it stops affecting pass/fail. Rejected on a key column (422 KEY_COLUMN_EXCLUDED_FROM_HASH): the proof phase compares an order-insensitive aggregate of the row hashes, so with the key outside the hash two datasets whose payloads are swapped between keys compare equal, and a proof-only mapping never runs the diff phase that would catch it. |
exclude_from_diff |
bool | Column still proves equality but is not reported cell-by-cell. Rejected on a key column (422 KEY_COLUMN_EXCLUDED_FROM_DIFF): the field diff finds each row by its key. A key column stored excluded before this rule still runs correctly, because the field diff always fetches the key. |
hash_column_order |
int | Position in the row serialization. Must match on both sides; normally leave to the platform. |
blob_compare_mode |
hash, metadata_only, skip, full |
How a BLOB/CLOB is compared. Defaults to hash (content-exact) when unset. metadata_only compares only length and existence - a same-length content change becomes invisible, so it is an explicit opt-in, never a default. The length unit is the category's: bytes for BLOB/binary (BLOB:<n>), code points for CLOB/text (CLOB:<n>) and for JSON / XML (SEMI:<n>, one render on the proof, the recheck and the field diff; JSON / XML metadata_only always runs on the reference implementation, never native - #2996). skip is rejected on a key column (422 KEY_COLUMN_BLOB_SKIPPED): it removes the column from the row hash, so payloads swapped between keys compare equal. |
sensitivity_level, mask_in_ui |
— / bool | Masks the value in evidence views. Field-level only. |
store_raw_diff_values |
bool | The third term of the AND-chain. Any FALSE above suppresses storage. |
per_field_diff_limit |
int | Overrides the per-column cap for this column only. |
transform_chain, transform_chain_target |
ordered list | Per-side normalizations applied before comparison (e.g. case folding). Five transforms deliberately force the reference implementation rather than engine SQL — see the transform matrix in CLAUDE.md. |
canonical_type_override |
date, timestamp, string |
Forces how a column is canonicalized when the engine's declared type is misleading. date / timestamp remap a date/timestamp column (inert on any other type). string declares that a ClickHouse FixedString(N) holds NUL-padded text, compared as text with the trailing NULs dropped; it is rejected on any other column type and with blob_compare_mode metadata_only / skip (422 CANONICAL_TYPE_OVERRIDE_NOT_APPLICABLE). A mapping with an effective override runs on the reference implementation on both sides unless that engine pair is verified for native rendering (none is for string). |
rounding_mode, timestamp_rounding |
mode | Rounding semantics for numerics/timestamps. Engines disagree on .5 boundaries, so this is explicit. |
dual_compare_mode, float_compare_mode |
mode | Qlik dual values and float comparison strategy. |
decrypt_algo, decrypt_params, decrypt_key_ref, decrypt_expression |
— | Encrypted-column handling. decrypt_algo/decrypt_params are API-read-only today (set them with the field-config import path). Keys are always resolved through the secrets backend and never stored. |
declared_type |
— | Derived — introspected from the engine, refreshed by schema-refresh. |
Compute and diff safety¶
| Option | Layers | Default (fresh install) | Reach | What it changes |
|---|---|---|---|---|
compute_mode |
L1, L4, L6, L7 | hybrid |
UI | Where comparison work happens (pushdown vs. platform-side). |
proof_only_mode |
L1, L4, L6, L7 | false |
UI | Stop after the proof phase. A failure becomes terminal fail_hash with no field-level diff — cheap, but no evidence of what differed. |
max_concurrency |
L1, L2, L3, L4, L6 (MIN) | 5 |
UI | Parallel queries against a system. |
max_partition_concurrency |
L4, L1, both L3 (MIN) | 4 |
UI | Parallel partitions in a partitioned run. |
query_timeout_seconds |
L1, L3, L4 | 300 |
UI | Per-query timeout. Size it for the proof query at full volume. |
max_diff_rows |
L1, L4 | 500000 |
UI | Cap on rows handed to the diff engine. (No schedule-level override exists.) |
diff_sampling_mode / diff_sample_size |
mode: L1, L4 · size: L4 only | all |
UI | Diff every mismatched row (all), a deterministic hash-selected N (sample_n), or percentage (sample_pct) of them. Selection is stable per run+key, not first-N. Sampled diffs are flagged is_sampled = TRUE in evidence and the total mismatch count is always recorded, so a sampled run still tells you the true scale of the difference. A sample_* mode with no diff_sample_size set on the mapping diffs 100% (the safe direction). |
egress_row_limit |
L1, L2, L4, L6 (MIN), L7 tighten-only | 1000000 |
UI | Max mismatched rows pulled back to the worker — the main egress-cost control. |
per_column_diff_limit |
same | 10000 |
UI | Max diff cells per column. |
global_diff_cell_limit |
same | 5000000 |
UI | Max diff cells for the whole run. |
fail_fast_threshold_pct |
L1, L2, L4, L7 | 50.00 |
UI | If this share of rows mismatch, skip the diff phase and fail immediately — the run is already conclusive. It ends fail_count if the row counts differ, otherwise fail_hash (never fail_field: no column was compared). |
diff_truncation_method |
L1 → L2 → L4 (L4 wins); L6/L7 may select only first_n / random_sample |
first_n |
UI | Which rows survive truncation once a limit binds. |
Readiness (when a comparison may begin)¶
Readiness is what makes Paridity CDC-aware: it refuses to compare a window that replication has not finished delivering, which is the difference between a real failure and a false alarm.
| Option | Layers | Default (fresh install) | Reach | What it changes |
|---|---|---|---|---|
readiness_mode |
L1, L4, L6 | hybrid |
API | How strictly CDC/watermark signals gate the run. |
max_lag_seconds |
L1/L4, MIN with L6 | 900 |
UI | Maximum tolerated replication latency before the run defers. |
quiescence_seconds |
L1, L4, L6 | 60 |
UI | How long the watermark must be stable before proof starts. |
retry_max_attempts |
L1, L4, L6 | 10 |
API | Readiness re-check attempts before the run is skipped. |
retry_backoff_seconds |
L1 | 60,120,300,600,900 |
Ops | Backoff ladder between re-checks. |
cdc_integration_mode |
L4 only | advisory |
UI | strict halts on CDC degradation; advisory warns and proceeds - except that a snapshot mapping is held (not compared) while a reachable replication tool reports a connector paused, stopped or failed; disabled skips CDC checks. Forced to disabled when no replication profile is attached. |
cdc_table_semantics |
L4 only | none |
UI | uniform_iceberg_mirror: the Iceberg side is a Databricks UniForm mirror of a Delta table; each run is held (not_ready, cause uniform_metadata_lag) until the Iceberg table property delta-version has reached the Delta version (the commit-instant comparison is the fallback when the property is unreadable; unreadable both ways holds). Needs an Iceberg commit_snapshot watermark on one side and a Delta one on the other (422 otherwise). replacing_merge_tree_final / clickpipes_peerdb (#2900, ClickHouse): the ClickHouse side holds unmerged row versions (ReplacingMergeTree and the other merge-dependent engines); every read of it goes through FINAL, and clickpipes_peerdb also excludes the ClickPipes soft-delete rows (_peerdb_is_deleted = 0). Needs a ClickHouse system on one side (422 CDC_TABLE_SEMANTICS_SYSTEM_MISMATCH otherwise); the run refuses with the reason if the table's engine or columns do not fit. none on a merge-dependent ClickHouse table is allowed but flagged (an advisory, not a block): unmerged versions are counted until they merge. none changes nothing. |
read_pin_views |
L4 only | false |
UI | Snowflake only (#2933). true: readiness also pins the mapping's Snowflake view side(s) at the readiness instant (AT(TIMESTAMP)), so every phase of a run reads the same state; the pinned read is verified first and a base table whose DATA_RETENTION_TIME_IN_DAYS is 0 refuses the run (a configuration hold naming that table). Needs a Snowflake system on one side (422 READ_PIN_VIEWS_SYSTEM_MISMATCH otherwise). Native tables need no flag: they are pinned when their retention is 1 day or more and read latest per phase at retention 0. false changes nothing. |
Evidence and retention¶
| Option | Layers | Default (fresh install) | Reach | What it changes |
|---|---|---|---|---|
evidence_kek_ref |
L2 only | (unset) | UI / API — platform admin only | Secrets-backend pointer (aws-secrets://, azure-keyvault://, gcp-secretmanager://, infisical://, vault://; env:// in development only) to the tenant secret that protects masked evidence values — the KEK that wraps each run's data key and the HMAC key behind the fingerprints are both derived from it. A pointer, never the key: the platform resolves it at run time and never stores or returns the material. While unset, masked diffs are written fingerprint-only and "Reveal masked values" is unavailable for every run of the tenant (the diff worker degrades to NULL rather than lose evidence, so nothing fails — the UI states the reason instead). Runs recorded before it was set stay fingerprint-only; a change takes effect on each mapping's next run. Set on Settings → Platform & Tenant Defaults → Evidence key (secrets pointer), which also shows Masked-value reveal: available / not configured; every change is audit-logged with the caller's IP. Added #2395 — until then this was DB-only (no API, no UI, no seed). |
store_raw_diff_values |
L2 AND L4 AND L5 | true |
UI (L4/L5) | Whether actual differing values are stored. Any FALSE in the chain stores NULL and sets values_masked. (The L1 column exists but is not read — it cannot suppress storage platform-wide.) |
retain_summary_days |
L4, else L1; floored by the L2 minimum | 540 |
UI | Retention for run summaries. |
retain_mismatch_keys_days |
L4, else L1; floored by the L2 minimum | 365 |
UI | Retention for mismatched-row keys. |
retain_field_diffs_days |
L4, else L1; floored by the L2 minimum | 180 |
UI | Retention for cell-level differences — usually the largest and shortest-lived evidence. |
bundle_retention_days |
L2, floored by L1 | 2555 |
UI | Retention for audit evidence bundles and their stored period containers. A bundle ages from its own generation time. The tenant value cannot go below the platform value. Enforced by the bundle leg of the same retention purge; GET /evidence-bundles/retention reports the resolved value, what awaits the next purge run, and the last purge on record. |
Evidence tables are append-only. Deletion happens only through the retention purge, which writes its own audit record of what it removed.
Legal holds override every value above. A legal hold (Settings → Legal Holds) freezes a tenant, a mapping, or a single bundle so the purge skips it — a held record is never deleted no matter how far past its retention window it is. Placing and releasing a hold is approval-gated (
evidence.legal_hold, a second person approves) and audited; the purge records each skip. A hold is an action, not a resolved-config field — it does not appear in the seven-layer resolution — but it is the control that lets you preserve evidence for litigation or an audit without changing anyone's retention numbers. Seedocs/runbooks/evidence_retention.md.The retention purge runs on a built-in schedule (#2025). These four settings define how long evidence is eligible to be kept; the platform's built-in scheduler (
RetentionPurgeWorkflow, a durable Temporal singleton on the scheduler worker) enforces them automatically. It is on by default but dry-run-first: for the firstretention_purge_dry_run_daysafter install (default 7) it computes and attests what would be deleted without deleting anything, so a fresh deployment never deletes on day one. Every fire writes onecfg.audit_logattestation row. An ad-hoc manual purge stays available and approval-gated (evidence.retention_purge).Deployment settings (env / Helm values, not per-tenant resolved config):
retention_purge_enabled(default on),retention_purge_interval_seconds(86400 = daily),retention_purge_hour_utc(4),retention_purge_dry_run_days(7),retention_purge_force_dry_run(off — keep dry-run indefinitely).Pausing the schedule is an approval-gated, audited operator act (
evidence.retention_purgeelevation →retention_purge.paused/.resumed), distinct from disabling it in deployment config. A paused schedule still fires but deletes nothing — the no-op is attested — so a paused schedule is a compliance factGET /evidence-bundles/retention.enforcementreports asretention_purge_paused. Pause and resume it in Settings → Retention Schedule. Seedocs/runbooks/evidence_retention.mdfor the full procedure.Legal holds (above) still override every value here: automatic deletion always skips a held scope. This is why the schedule ships default-on safely — evidence under a preservation order can always be frozen.
Windows, watermarks and partitions¶
| Option | Layers | Reach | What it changes |
|---|---|---|---|
windowing_mode |
L4 | API | fixed_interval (a rolling time window), watermark_range (bounded by observed watermarks), or partitioned_snapshot (per-partition frozen snapshots). |
watermark_domain |
L4 | UI | time_utc or sequence. Never mix domains within a mapping — an SCN and a timestamp are not comparable. |
| Source/target watermark config | L4 | UI | Strategy and field per side. Prefer, in order: control_marker, delta_version, ingest_ts, partition_max, timestamp, custom_sql, then snapshot modes. |
window_interval_seconds, min_advance_seconds |
L4 | API | Window size and the minimum advance required for a new run to be worth executing. |
partition_strategy |
L4 | UI | time (with a grain), key_range, list, or composite (two components, no nesting). Per-partition SQL scoping is decided per (engine, kind, column-type family) cell and per side — see the matrix below: on an unverified cell the partition is proven over the whole window on that side (correct, no speed benefit). The mapping page shows the per-side prediction; the run page shows what each run recorded. |
max_partitions_per_run |
L1 only | UI | Safety cap on partitions per run (default 256). |
window_mode |
L6 | UI | auto (incremental) or full (forced full-table comparison). |
Per-partition SQL scoping — the live-verified cells (generated from src/services/partition_coverage.py by scripts/gen_partition_scoping_matrix.py; do not hand-edit). Scoping is decided per side of a mapping and per (engine, partition kind, column-type family) cell: a partition's proof and extract SQL are scoped to that partition on a side only when the side's engine has a live-verified cell for the partition's kind and the partition column's type family — on any other cell the partition's proof runs over the whole window on that side (results stay correct: the coverage check still holds; the per-partition speed benefit does not apply). A composite partition is scoped only when every component is. The mapping page shows the pre-run prediction per component and side; the run page shows what each run actually recorded.
| Engine | time |
is_null |
key_range |
list |
|---|---|---|---|---|
bigquery |
date, timestamp |
null |
numeric, date |
numeric, date, string |
databricks |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
db2_luw |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
mysql |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
oracle |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
postgres |
time, date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
redshift |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
sap_hana |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
snowflake |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
sqlserver |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
trino |
date, timestamp |
null |
numeric, date, timestamp |
numeric, date, timestamp, string |
Family names: time = a family-less legacy time predicate (PostgreSQL's type-agnostic cell); null = the NULL bucket; numeric / date / timestamp / string = the partition column's source-introspected type family (predicate_type_family). A column whose type the schema snapshot does not know has no family and is never scoped (refresh the schema).
time partitions under a non-UTC global timezone additionally need the side's comparison frame (classified when the run starts, from the column's type) to be live-verified for the engine — so the mapping page reports such a component as classified at run time until a run records it. PostgreSQL keeps its own per-side cast path and needs no frame cell.
| Engine | verified frames (tz ≠ UTC) |
|---|---|
bigquery |
utc_frame, wall_frame |
databricks |
utc_frame, wall_frame |
db2_luw |
wall_frame |
mysql |
utc_frame, wall_frame |
oracle |
utc_frame, wall_frame |
postgres |
own per-side cast path (any classified frame) |
redshift |
utc_frame, wall_frame |
sap_hana |
wall_frame |
snowflake |
utc_frame, wall_frame |
sqlserver |
utc_frame, wall_frame |
trino |
utc_frame, wall_frame |
key_range / list partitions on a timestamp column under a non-UTC global timezone (#2329) are rendered per side frame too — a naive authored bound means local wall-clock time in the tenant timezone, an offset-bearing bound the instant as written — but the >= / < / = bound shapes are verified SEPARATELY from time partitions, per (engine, kind, frame); PostgreSQL has no exemption here. An engine with no cell for the kind keeps such a partition an honest whole-window proof on that side (reason unverified_bound_frame); an engine with a cell reports the component as classified at run time until a run records the frame.
| Engine | key_range (tz ≠ UTC, timestamp column) |
list (tz ≠ UTC, timestamp column) |
|---|---|---|
bigquery |
— | — |
databricks |
— | — |
db2_luw |
wall_frame |
wall_frame |
mysql |
utc_frame, wall_frame |
utc_frame, wall_frame |
oracle |
utc_frame, wall_frame |
utc_frame, wall_frame |
postgres |
utc_frame, wall_frame |
utc_frame, wall_frame |
redshift |
— | — |
sap_hana |
— | — |
snowflake |
— | — |
sqlserver |
utc_frame, wall_frame |
utc_frame, wall_frame |
trino |
utc_frame, wall_frame |
utc_frame, wall_frame |
Reason codes (the reason / explanation fields on GET /runs/{id} → partition_scoping, GET /runs/{id}/partitions → scoping_detail and GET /mappings/{id} → partition_scoping; a composite reports composite_component:<index>:<reason> for its first failing component):
| Reason | What it means |
|---|---|
composite_shape |
The composite partition is malformed (a nested composite, a non-object component, or no components); the partition's proof ran over the whole window. |
frame_classified_at_run |
Partition scoping for a partition kind partition on a timestamp column under a non-UTC global timezone depends on the affected partition column's comparison frame, which is classified when the run starts; until a run records it the mapping reports scoping as not applied. |
no_side_column |
The partition predicate names no affected-side column (declare source_column and target_column on the strategy); the partition's proof ran over the whole window. |
no_typed_spelling |
The the engine engine has no exact literal spelling for this column-type partition kind bound (a bound that does not parse in the column's type, a missing bound, or a string list without a known trim setting); the partition's proof ran over the whole window. |
quantize_unverified |
This column-type partition kind partition compares a column whose canonical value is rounded (or truncated) before hashing, and quantized membership on the engine has not been live-verified (only PostgreSQL half-up rounding is); a raw-column bound could place a boundary row in different partitions on the two sides, so the partition's proof ran over the whole window. |
timestamp_bound_non_utc_tz |
This run was recorded before per-frame rendering of timestamp partition kind bounds existed (#2320, superseded by #2329): under a non-UTC global timezone the bounds could not be rendered per comparison frame (wall-clock vs instant), so the partition was not scoped on any engine; re-run the mapping to get per-frame scoping where the engine is verified; the partition's proof ran over the whole window. |
unclassified_frame |
Under a non-UTC global timezone the affected partition column's comparison frame could not be classified (wall-clock vs instant), so a time bound cannot be rendered safely; the partition's proof ran over the whole window. |
unknown_family |
The partition column's type is not known to the mapping's schema snapshot (refresh the schema, or partition on a mapped column), so no typed boundary literal can be rendered; the partition's proof ran over the whole window. |
unsupported_kind |
This partition predicate is not one of the scopable kinds (time, is_null, key_range, list, or a composite of those); the partition's proof ran over the whole window. |
unverified_bound_frame |
The the engine engine has not been live-verified to render a timestamp partition kind partition against a comparison-frame column under a non-UTC global timezone (the >= / < / = bound shapes are verified separately from time partitions); the partition's proof ran over the whole window. |
unverified_cell |
The the engine engine has not been live-verified to place every boundary row of a column-type partition kind partition exactly; the partition's proof ran over the whole window. |
unverified_frame |
The the engine engine has not been live-verified to render a time partition against a comparison-frame column under a non-UTC global timezone; the partition's proof ran over the whole window. |
Schedules¶
Schedules are interval-based, not cron. Set in Schedules; all UI.
| Option | Values | What it changes |
|---|---|---|
interval_value + interval_unit |
int + minutes/hours/days/weeks |
Recurrence. |
days_of_week |
ISO 1–7, or empty | Restrict to specific days. |
active_start_time, active_end_time |
time | Daily active window. |
effective_from, effective_until |
date | Calendar bounds — how a quarter-end or year-end schedule is expressed. |
tags |
list | Grouping used by close mode. |
priority |
low, normal, high, critical |
Ordering when the platform is congested. Resolved as MAX with the platform floor. |
window_mode |
auto, full |
Forces a full comparison for this schedule (e.g. a quarter-end sweep). |
enabled |
bool | Master toggle. |
Close mode is a bulk convenience toggle over tags, not a separate scheduling
engine: it enables/disables by tag, may step priority by −3..+3 enum levels, and
may apply the two tightening overrides (max_lag_seconds,
quiescence_seconds). Overrides looser than a schedule's mapping are skipped
per schedule and reported back. Available at Configure → Schedules → Close
Mode, and as POST /schedules/bulk-toggle. See
Schedules and close mode.
Deployment settings¶
Set as environment variables by your operator; not part of run configuration.
The complete list lives in src/config/settings.py. The ones a deployer must
understand:
| Variable | Purpose |
|---|---|
DATABASE_URL |
Control-plane Postgres. Required, no default. |
APP_ENV |
development / test / local / production. Must be set explicitly — the dev auth bypass is refused unless this names a non-production environment. |
TEMPORAL_HOST, TEMPORAL_NAMESPACE |
Workflow engine. |
SECRETS_BACKEND |
aws, azure, gcp, infisical, or vault. |
OIDC_ISSUER, OIDC_AUDIENCE, OIDC_JWKS_BASE_URL |
Identity provider. |
SCIM_AUDIENCE, MCP_AUDIENCE |
Dedicated audiences for the provisioning and agent surfaces. A token minted for one surface is rejected on the others. |
PUBLIC_BASE_URL |
Used in SCIM resource URLs and OAuth metadata. |
CFG_AI_ENABLED, CFG_AI_REQUIRE_TENANT_OPT_IN, MCP_RATE_LIMIT_PER_MINUTE |
AI/agent surface controls. Production forces tenant opt-in. |
AUDIT_SIGNING_KEY_REF |
Signing key for the audit hash-chain anchor. |
AUDIT_SIGNING_PREVIOUS_KEY_REFS |
Retired signing keys, verify-only (comma-separated). Move the outgoing ref here when you rotate: a checkpoint is verified under the key that signed it, and only declared keys are trusted. Keys left out are reported as unverified on GET /audit/verify — not as tampering, but no longer provable either. |
Related¶
- Glossary — Mapping, Replication Profile, watermark,
hash_totalvsrow_hash. - Float column handling — the one canonicalization topic with its own guide.
docs/specs/spec_canonical_hashing.md— the full canonicalization contract.docs/specs/spec_watermark_config.md— per-strategy watermark configuration.