Hero is now a data-native 0pt artifact (no photos, keeps grain/reveal/float); header is the 0pt wordmark with Swedish nav; layout is sv + loads JetBrains Mono + 0pt metadata; globals wires the loaded fonts. Adds CLAUDE.md + docs/01-09 as the build contract. next build green (5 routes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.2 KiB
8.2 KiB
03 · Data model
Postgres is the backbone. Two halves: data entities (what we sell) and platform tables (orders, users, jobs). Every fact links to bridge_provenance. Migrations via Alembic (Python) or sql/ migrations checked into the repo — pick one and be consistent; align with existing src/workers/db.py.
Conventions
snake_case, UTC timestamptz, soft-delete viadeleted_atwhere useful.- Natural keys for registries (
org_nr), surrogateuuidPKs for platform tables. created_at/updated_aton every table.- All monetary amounts in öre (integer) to avoid float errors; currency
SEKdefault.
Data entities
-- A company (the core registry entity)
CREATE TABLE dim_company (
org_nr text PRIMARY KEY, -- national reg. number (per market format)
market text NOT NULL, -- ISO country code: SE/NO/DK/FI/...
lei text, -- GLEIF cross-border key
name text NOT NULL,
status text, -- active/dissolved/...
nace text, -- NACE/SNI code
registered_at date,
address jsonb,
website text,
updated_at timestamptz NOT NULL DEFAULT now(),
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE fact_financials (
org_nr text REFERENCES dim_company(org_nr),
fiscal_year int NOT NULL,
revenue bigint, -- öre
result bigint,
employees int,
source_id text REFERENCES dim_source(source_id),
PRIMARY KEY (org_nr, fiscal_year)
);
CREATE TABLE dim_contact (
contact_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_nr text REFERENCES dim_company(org_nr),
name text,
role text,
email text,
phone text,
linkedin text,
-- PII: subject to GDPR; lawful basis recorded per delivery
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE dim_ubo ( -- ultimate beneficial owner
ubo_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_nr text REFERENCES dim_company(org_nr),
owner text,
share numeric(5,2)
);
CREATE TABLE dim_vehicle ( -- VehIQ synergy
vin text PRIMARY KEY,
market text NOT NULL,
reg_plate text,
make text, model text, year int,
owner_org text REFERENCES dim_company(org_nr),
specs jsonb,
updated_at timestamptz NOT NULL DEFAULT now()
);
Provenance model (the signature — every fact links here)
CREATE TABLE dim_source (
source_id text PRIMARY KEY, -- 'gleif','brreg','companies_house',...
name text NOT NULL,
market text, -- or 'global'
access text, -- 'api'|'bulk'|'authorized_web'|'licensed'
licence text, -- short licence note
cadence text, -- 'daily'|'weekly'|'annual'|...
url text
);
-- One row per (entity field) we deliver, linking value → where it came from + confidence
CREATE TABLE bridge_provenance (
field_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
entity_type text NOT NULL, -- 'company'|'contact'|'vehicle'|'financials'
entity_key text NOT NULL, -- org_nr / vin / contact_id
field_name text NOT NULL, -- 'revenue','email',...
source_id text REFERENCES dim_source(source_id),
freshness date, -- when this value was confirmed
confidence numeric(4,3) NOT NULL, -- 0.000–1.000
level smallint NOT NULL, -- 0 unavailable/licensed, 1 single, 2 partial, 3 corroborated
agreed_sources int DEFAULT 1, -- # independent sources that agree
UNIQUE (entity_type, entity_key, field_name)
);
CREATE INDEX ON bridge_provenance (entity_type, entity_key);
Rule: no value ships without a
bridge_provenancerow. Level 0 = delivered as "not available / licensed", never a guessed value.
Platform tables
CREATE TABLE orgs (
org_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL, created_at timestamptz DEFAULT now()
);
CREATE TABLE users (
user_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
email text UNIQUE NOT NULL,
role text NOT NULL DEFAULT 'member', -- 'owner'|'admin'|'member'|'siax_admin'
created_at timestamptz DEFAULT now()
);
CREATE TABLE orders (
order_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
flow text NOT NULL, -- 'catalog'|'custom'|'subscription'
status text NOT NULL DEFAULT 'draft', -- see lifecycle in docs/01
config jsonb NOT NULL, -- configurator state (markets, fields, ...)
total_ore bigint, deposit_ore bigint, balance_ore bigint,
est_days int, est_confidence numeric(4,3),
created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now()
);
CREATE TABLE order_items ( -- resolved line items / dataset refs
item_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
kind text, ref text, qty int, meta jsonb
);
CREATE TABLE jobs ( -- one production run per order
job_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
stage text, -- sourcing|extraction|structuring|enrichment|qa
pct numeric(5,2) DEFAULT 0,
state text DEFAULT 'queued', -- queued|running|done|failed
metrics jsonb, -- rows, confidence, per-market progress
started_at timestamptz, finished_at timestamptz
);
CREATE TABLE deliveries (
delivery_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
formats text[], destinations text[],
record_count int, avg_confidence numeric(4,3),
quality_report jsonb, provenance_export_url text,
files jsonb, -- [{format,url,bytes}]
created_at timestamptz DEFAULT now()
);
CREATE TABLE subscriptions (
sub_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
cadence text, -- weekly|daily
webhook_url text, status text DEFAULT 'active',
next_run_at timestamptz
);
CREATE TABLE files ( -- customer uploads
file_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
order_id uuid REFERENCES orders(order_id),
name text, mime text, bytes bigint, r2_key text,
created_at timestamptz DEFAULT now()
);
CREATE TABLE db_grants ( -- customer read-only DB access (encrypted)
grant_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
org_id uuid REFERENCES orgs(org_id),
order_id uuid REFERENCES orders(order_id),
conn_encrypted bytea NOT NULL, -- encrypted connection string
scope text, expires_at timestamptz, revoked_at timestamptz,
created_at timestamptz DEFAULT now()
);
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
actor text, action text, target text, meta jsonb,
at timestamptz DEFAULT now()
);
CREATE TABLE payments (
payment_id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
order_id uuid REFERENCES orders(order_id),
kind text, -- 'deposit'|'balance'
amount_ore bigint, stripe_ref text,
status text, -- 'pending'|'captured'|'released'|'failed'
created_at timestamptz DEFAULT now()
);
pgvector
CREATE EXTENSION vector; Use for entity matching/dedup and for the AI-assist (semantic match of NL request → catalog/config). Store embeddings in a embeddings table keyed by (entity_type, entity_key) or alongside catalog rows.
Row-level security & catalog
- Catalog products: a
catalogtable (or view) over curated datasets with sample rows, counts, coverage, price. - Per-customer data isolation enforced by
org_idfiltering in the API and (where exposed via NocoDB/Metabase) by scoped views/connection params, plus Postgres RLS where practical.