Current schema (admin/admin.db), target schema additions (audit trail, RBAC keys), PFP rule JSON schema, ingestion YAML schema, and DeltaPrism migration path. PostgreSQL is the medium-term production target.
Database: admin/admin.db (SQLite). Shared by FastAPI engine and Flask admin panel via filesystem. Both services use plain sqlite3 or SQLAlchemy respectively — no ORM dependency in the engine.
| Table | Rows | Purpose |
|---|---|---|
_tbl_calcrule_profiles | 4 | Versioned domain-scoped rule containers |
_tbl_calcrule_rules | 1,213+ | Individual calculation rules (229 TCO + 535 PFP formula + 430 PFP table + 19+ EDR) |
_tbl_nexus_platform_users | live | Platform users registered via Google OAuth 2.0 — role-based access control |
Stores calculation profiles. Each profile is a versioned, domain-scoped container for rules. At most one profile can be active per domain.
CREATE TABLE _tbl_calcrule_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT UNIQUE NOT NULL,
-- 'tco-dc-cooling', 'pfp-clearance-hdb', 'edr-entity-validation'
name TEXT NOT NULL,
-- 'TCO Calculator — Data Centre Cooling'
description TEXT,
technology_variants TEXT DEFAULT '[]',
-- JSON array: '["traditional","rdhx","dlc","grc"]'
domain TEXT NOT NULL DEFAULT 'tco',
-- Bounded context: 'tco' | 'edr' | 'pfp' | 'pfp_table'
is_active INTEGER NOT NULL DEFAULT 0,
-- 1 = active within domain. At most one active per domain.
version TEXT DEFAULT '1.0.0',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
Current rows:
| id | slug | domain | rules | is_active |
|---|---|---|---|---|
| 1 | tco-dc-cooling | tco | 229 | 1 |
| 2 | pfp-clearance-hdb | pfp | 535 | 1 |
| 3 | pfp-table | pfp_table | 430 | 1 |
| 4 | edr-entity-validation | edr | 19+ | 1 |
Stores individual calculation rules. Each rule belongs to exactly one profile. Rules are executed in descending priority order within a pipeline run.
CREATE TABLE _tbl_calcrule_rules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
profile_id INTEGER NOT NULL REFERENCES _tbl_calcrule_profiles(id) ON DELETE CASCADE,
rule_id TEXT NOT NULL,
-- Logical ID: 'pfp_hdb_clr_002', 'infra_001_servers_per_rack'
name TEXT NOT NULL,
formula TEXT NOT NULL,
-- Python expression evaluated in sandboxed namespace
output TEXT,
-- Dot-path output field: 'result.clearance_mm', 'capex.racks_capex'
rule_type TEXT,
-- 'pfp_clearance' | 'pfp_selection' | 'pfp_sza'
-- 'infrastructure' | 'capex' | 'opex' | 'tco' | 'scoring' | 'edr_validation'
technology TEXT,
-- NULL = all; 'traditional' | 'rdhx' | 'dlc' | 'grc' | 'hdb' | 'fs'
priority INTEGER DEFAULT 0,
-- Execution order: higher runs first
enabled INTEGER NOT NULL DEFAULT 1,
skip_if_set INTEGER NOT NULL DEFAULT 0,
-- If 1: skip when output field already has a non-None value
excel_source TEXT,
excel_baseline REAL,
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE (profile_id, rule_id)
);
Audit trail for rule mutations. Write one row per changed field in every PATCH handler before applying the update. Enables GET /rules/{id}/history.
CREATE TABLE _tbl_calcrule_rule_audit (
id INTEGER PRIMARY KEY AUTOINCREMENT,
rule_id TEXT NOT NULL,
-- Logical rule_id (not FK — preserves history after delete)
profile_id INTEGER,
changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
changed_by TEXT,
-- API key prefix (first 8 chars) or 'admin-panel'
operation TEXT NOT NULL,
-- 'CREATE' | 'PATCH' | 'DELETE'
field TEXT,
-- Field name changed (NULL for CREATE/DELETE)
old_value TEXT,
-- JSON-serialised previous value
new_value TEXT
-- JSON-serialised new value
);
CREATE INDEX idx_audit_rule_id ON _tbl_calcrule_rule_audit(rule_id);
CREATE INDEX idx_audit_changed_at ON _tbl_calcrule_rule_audit(changed_at);
RBAC key store. Keys stored as SHA-256 hashes. Per-key scopes and expiry. Replace env-var comparison in require_api_key with hash lookup against this table.
CREATE TABLE _tbl_api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_hash TEXT UNIQUE NOT NULL,
-- SHA-256 of the raw key — never store plaintext
client_id TEXT NOT NULL,
-- 'pyrometrix-revit', 'admin', 'ci-pipeline'
scopes TEXT NOT NULL DEFAULT '["compute"]',
-- JSON array: ["compute"] | ["compute","rules:read"] | ["admin"]
expires_at DATETIME,
-- NULL = never expires (dev/internal only)
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_used DATETIME,
enabled INTEGER NOT NULL DEFAULT 1
);
Format used in nexus/rules/pfp/*.json. These files are import-format only — the DB is the runtime source of truth after seeding.
{
"id": "pfp_hdb_clr_002",
"name": "HDB EI60 Clearance — Double Drywall",
"type": "pfp_clearance",
"technology": "pfp",
"priority": 6998,
"formula": "result.clearance_mm = 25 if (...) else None",
"output": "result.clearance_mm",
"skip_if_set": true,
"enabled": true,
"description": "Clearance for EI60 rated HDB penetration through DBL DW",
"ruleset": "HDB"
}
| Field | Required | Description |
|---|---|---|
id | Yes | Logical rule ID, unique within profile |
name | Yes | Human-readable name |
type | Yes | Rule type: pfp_clearance, pfp_selection, pfp_sza |
priority | Yes | Execution order (higher = first) |
formula | Yes | Python expression evaluated in sandboxed namespace |
output | Yes | Dot-path output field target |
skip_if_set | No | If true: skip when output field already has a non-None value |
technology | No | Scope limiter: hdb or fs |
ruleset | No | Source ruleset label: HDB or FS |
Lookup-table format used in nexus/rules/pfp/table_*.json. Row matching uses hierarchical specificity: cat_a → cat_g → cat_c → position. More specific matches win over general matches.
{
"id": "tbl_hdb_001",
"type": "pfp_table_row",
"s1": { "cat_a": "Pipe", "cat_g": "Steel", "cat_c": null },
"s2": { "cat_a": "Wall", "cat_g": "Concrete", "cat_c": null },
"fire_rating": "EI60",
"position": null,
"clearance_mm": 25,
"top_mm": 25,
"side_mm": 25,
"bottom_mm": 25
}
Files in nexus/config/datasources/*.yaml drive IngestionPipeline. The type field selects the DataSource connector backend.
name: edr-uatdb
description: "EDR v6 UAT2 SQLite database"
pipeline: edr
source:
type: sqlite # sqlite | csv | json | excel | access | postgres | odbc | rest
path: "/path/to/db"
extractors:
- id: equipment
table: FACT_100_Equip_UAT2
entity_class: Equipment
limit: 2000
field_map:
Equipment_Tag: entity.tag
Description: entity.description
value_filters:
Status: ["Active", "Commissioned"]
class_rules:
- field: entity.tag
pattern: "^EL[-_]"
entity_class: Electrical
Supported connector types: sqlite, csv, json, excel, access (.accdb via pyodbc), postgres, odbc, rest
Target architecture: rules stored as graph nodes in DeltaPrism property graph database. Enables dependency analysis, version history as edges, and conflict detection between rules.
Node: Rule
Properties: id, name, formula, priority, output, type, technology, skip_if_set
Edges:
BELONGS_TO → Profile node
DEPENDS_ON → Rule node (formula references output of another rule)
SUPERSEDES → Rule node (version history)
CONFLICTS_WITH → Rule node (same output, overlapping conditions)
Node: Profile
Properties: slug, name, domain, version, is_active
Edges: HAS_RULE → Rule
Node: Domain
Properties: key (tco | edr | pfp), description
Edges: HAS_PROFILE → Profile
Integration: profile_store.py gains a NEXUS_RULE_STORE=deltaprism backend switch. The existing connector layer (datasource.py) can reach DeltaPrism via ODBC or REST. No changes to the rule execution engine or API surface required.
Platform-level user registry. Every user who completes Google OAuth for the first time gets a row. Org-level access is governed separately by org_user. Role nexus_admin grants access to all nav groups and platform management. Added 2026-05-11, deployed as engine task def nexus-prod-engine:10. Login is broken in production (ISSUE-073 — see _STATE_STORE root cause). All auth/user management endpoints implemented in Wormwood routes_auth.py.
CREATE TABLE _tbl_nexus_platform_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
google_sub TEXT NOT NULL UNIQUE, -- Google subject identifier (immutable)
email TEXT NOT NULL UNIQUE, -- verified Google email address
display_name TEXT, -- full name from Google profile
picture_url TEXT, -- Google profile photo URL (max 512 chars)
nexus_role TEXT NOT NULL DEFAULT 'nexus_user',
-- 'nexus_user' | 'nexus_admin'
is_active INTEGER NOT NULL DEFAULT 1,
-- 0 = account deactivated (login blocked)
role_updated_at DATETIME, -- timestamp of last role change (UTC)
role_updated_by TEXT, -- email of admin who last changed the role
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_login_at DATETIME -- updated on every successful login
);
CREATE UNIQUE INDEX uq_npu_google_sub ON _tbl_nexus_platform_users (google_sub);
CREATE UNIQUE INDEX uq_npu_email ON _tbl_nexus_platform_users (email);
| Column | Type | Constraints | Description |
|---|---|---|---|
id | INTEGER | PK, AUTOINCREMENT | Internal surrogate key. Used as sub in Nexus JWT. |
google_sub | TEXT(128) | UNIQUE, NOT NULL, indexed | Google’s stable user identifier from id_token.sub. Never changes even if email changes. |
email | TEXT(255) | UNIQUE, NOT NULL, indexed | Verified Google email. Used to match legacy rows on first OAuth login. |
display_name | TEXT(200) | NULL | Full name from Google profile. Updated on login if Google picture_url changes. |
picture_url | TEXT(512) | NULL | Google profile photo CDN URL. Refreshed on every login if changed. |
nexus_role | TEXT(30) | NOT NULL, default nexus_user | Platform RBAC role. Values: nexus_user (default), nexus_admin. Overridden by NEXUS_ADMIN_EMAILS on every login. |
is_active | INTEGER (bool) | NOT NULL, default 1 | Account activation flag. 0 = deactivated; login blocked at OAuth callback with HTTP 403 / code account_deactivated. Set by admin via PATCH /auth/users/{id}/active. Data is retained when deactivated (no DELETE). Requires migration: ALTER TABLE ... ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1. |
role_updated_at | DATETIME | NULL | Timestamp (UTC) of the last nexus_role change. Set by PATCH /auth/users/{id}/role. NULL on initial registration. |
role_updated_by | TEXT(255) | NULL | Email of the nexus_admin who last changed this user’s role. Stored from the JWT email claim of the requesting admin. Provides audit trail without a separate audit log table. |
created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | First registration timestamp (UTC). |
last_login_at | DATETIME | NULL | Updated on every successful GET /auth/callback completion. |
1. Query by google_sub 2. If not found: query by email (legacy row migration) 2a. If found: set google_sub on existing row 2b. If not found: INSERT new row with nexus_role='nexus_user' 3. If email in NEXUS_ADMIN_EMAILS: SET nexus_role='nexus_admin' 4. UPDATE last_login_at = NOW(UTC) 5. UPDATE picture_url if changed 6. COMMIT
{
"sub": <user.id>, // integer
"email": <user.email>,
"name": <user.display_name>,
"role": <user.nexus_role>, // 'nexus_user' | 'nexus_admin'
"iat": <unix timestamp>,
"exp": <iat + 28800> // 8-hour validity
}
| Method | Path | Auth | Response |
|---|---|---|---|
| GET | /auth/google | None | 302 → Google consent screen |
| GET | /auth/callback | None (OAuth) | 302 → SPA #token= or #auth_error= |
| GET | /auth/me | Bearer JWT | {id, email, display_name, picture_url, nexus_role, created_at, last_login_at} |
| PATCH | /auth/me | Bearer JWT | Updated user dict |
| GET | /auth/users | Bearer JWT (nexus_admin) | Array of all NexusPlatformUser records (REQ-AUTH-005 — IMPLEMENTED, ISSUE-073 pending) |
| PATCH | /auth/users/{id}/role | Bearer JWT (nexus_admin) | Updated user; 409 if self-demotion (REQ-AUTH-005 — IMPLEMENTED, ISSUE-073 pending) |
| PATCH | /auth/users/{id}/active | Bearer JWT (nexus_admin) | Updated user; 409 if self-deactivation (REQ-AUTH-005 — IMPLEMENTED, ISSUE-073 pending) |
Columns is_active, role_updated_at, role_updated_by are defined in admin/models.py and were added to the production database as part of engine:10 deployment (ISSUE-073). SQLAlchemy create_all() does NOT add columns to existing tables — explicit ALTER TABLE migration was required.
-- migrate_m_nexus_06.py (executed — engine:10) ALTER TABLE _tbl_nexus_platform_users ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1; ALTER TABLE _tbl_nexus_platform_users ADD COLUMN role_updated_at DATETIME; ALTER TABLE _tbl_nexus_platform_users ADD COLUMN role_updated_by TEXT;