eu-complyv1

Internal — engineering only

daducks: platform docs

The single source of truth for engineers working on the Compliance Toolkit platform. If you are integrating against the public API, you want the API Docs page instead.

What is daducks

daducks is our internal codename for the platform engineering docs — the architecture, invariants, and operational runbook behind InvGuard, rify-AI, and the Compliance Toolkit. It is intentionally not linked from external-facing surfaces.

Governing decision records live in docs/adr/: ADR-0001 (compliance-by-design three-component architecture) and ADR-0002 (persistence & service hardening). Read those first; this page is the operational companion.

Architecture

One data flow, three components. InvGuard is the data source, the Compliance Toolkit is the product surface, rify-AI is the trust substrate that makes both defensible.

textInvoice ──▶ InvGuard (score + explain)
                 │  DecisionRecord
                 ▼
           rify-AI (canonical hash → sign → anchor; content → IPFS/DB)
                 │  AnchorProof
                 ▼
           Compliance Toolkit (model card · bias · audit report)
                 │
                 ▼
           Postgres (durable decision trail)

Services are pure (CPU-only, no I/O): scoring, hashing, and report templating take inputs and return values. All persistence goes through a repository over a request-scoped async session. This keeps the v2 swaps (real Neo4j / IPFS / on-chain) localized to the edges.

Repository layout

textapp/
  api/            routes (v1/router.py) + middleware (request id, rate limit)
  core/           config, logging, errors, security
  db/             SQLAlchemy Base, models, async session
  domain/         Pydantic schemas (invguard, rify, compliance)
  repositories/   DecisionRepository (persistence gateway)
  services/       invguard · rify · compliance (pure business logic)
  main.py         app factory: lifespan, CORS, middleware, handlers
migrations/       Alembic (async env.py) + versions/
tests/            pytest (async, in-memory SQLite)
docs/adr/         architecture decision records
dashboard/        this Next.js app

Local development

Backend

powershellpython -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -e ".[dev]"
copy .env.example .env
uvicorn app.main:app --reload   # http://127.0.0.1:8000/docs

Defaults use SQLite with APP_AUTO_CREATE_TABLES=true, so no migration step is needed for a first run. Point APP_DATABASE_URL at Postgres to mirror prod.

Dashboard

bashcd dashboard
npm install
cp .env.local.example .env.local   # set BACKEND_URL
npm run dev                        # http://localhost:3000

The dashboard proxies the backend server-side (see lib/api.ts), so the browser never talks to FastAPI directly and API keys stay off the client.

Backend services

InvGuard (services/invguard.py)

Deterministic rule engine over the invoice. Each fired rule contributes a weight; the score is min(1.0, Σ weights), mapped to risk bands (≥0.7 high → block, ≥0.35 medium → review, else approve). The v1 rule set is small and explicit on purpose — the AI Act wants documented decision logic, not "the model said so". SHAP and the trained GNN are v2 (see roadmap).

Compliance (services/compliance.py)

Templated document generation only — never free-form LLM output, to avoid hallucinated compliance claims. The bias report groups by the real submitted vendor_regionas an explicitly-labeled proxy, and states the "no natural protected-class attribute" caveat in-band.

Data model & persistence

One table, decisions. Queryable columns are promoted for indexing/reporting; the immutable record is retained as JSON; anchored_content holds the canonical committed payload that verification re-hashes.

textdecisions
  decision_id      PK
  invoice_id       idx
  model_version    idx
  score            float
  risk_level       idx   decision  idx
  vendor_region          company_size          (proxy attrs for bias)
  anchor_cid       idx   anchored_content text  (IPFS stand-in)
  record_json      json  (full DecisionRecord)
  created_at       idx

Invariant: the repository reconstructs domain objects fromrecord_json via DecisionRecord.model_validate. This is why a stray attribute won't round-trip — the record must be a realDecisionRecord (a declared proof field), not aDecision with an extra attribute. That exact bug was caught by the persistence tests.

Migrations

bashalembic upgrade head                      # apply
alembic revision --autogenerate -m "msg"  # create
alembic check                             # CI drift gate — must be clean

rify-AI internals

The trust layer has one hard requirement: reproducibility. The steps:

  • Canonical content — a JSON serialization of the committed fields withsort_keys=True and compact separators. Never str(dict); ordering and whitespace must be stable across processes/machines.
  • Commitment — sha256(canonical_content).
  • Signature — HMAC-SHA256 keyed by APP_SIGNING_SECRET (the secret, not the key id) over the commitment.
  • Anchor — commitment + signature + ipfs_cid +transaction_hash. Content is stored (DB now, IPFS in v2).

Verification re-hashes the stored content and compares, then checks the signature — returning valid / tampered / signature_invalid / unanchored. The tamper test corrupts anchored_content directly and asserts detection.

Known trust gap: a single backend signing key (ADR-0001/0002). If it leaks, signatures can be forged. Roadmap: multi-sig / HSM-backed signing.

Configuration reference

Env-driven via pydantic-settings, prefix APP_. Production boot is guarded.

textAPP_ENVIRONMENT          development | staging | production
APP_DATABASE_URL         async SQLAlchemy URL (sqlite+aiosqlite | postgresql+asyncpg)
APP_AUTO_CREATE_TABLES   true for dev/CI; false in prod (use Alembic)
APP_SIGNING_SECRET       rify-AI HMAC secret (must be strong in prod)
APP_SIGNING_KEY_ID       key identifier (public)
APP_API_KEYS             CSV/JSON; empty = writes open (dev)
APP_CORS_ORIGINS         CSV/JSON; no "*" in prod
APP_RATE_LIMIT_*         enabled / requests / window_seconds
APP_LOG_LEVEL            APP_JSON_LOGS

The production guard refuses to start with the default signing secret, a SQLite URL, or a wildcard CORS origin. Do not weaken it to make a deploy pass — fix the env.

Dashboard internals

  • Server Components (overview, compliance) fetch live data via lib/api.ts with cache: no-store and dynamic = "force-dynamic".
  • Route Handlers under app/api/* proxy interactive writes/verify, injecting BACKEND_API_KEY server-side.
  • Client Components (DecisionsWorkbench) call only same-origin /api/* — no CORS, no secrets in the bundle.
  • lib/api.ts imports server-only so it can never be bundled into client code.

Testing & CI

Backend tests run async against in-memory SQLite (StaticPool keeps the schema alive across sessions). The suite covers scoring bands, validation envelopes, pagination, tamper detection, canonical-hash determinism, and the production config guards.

bash# backend
pytest            # 18 tests
ruff check .      # lint
mypy app          # strict types
# CI additionally runs: ruff format --check, alembic upgrade + alembic check

Deployment & runbook

Containers

Multi-stage Dockerfile, non-root, gunicorn + uvicorn workers. The entrypoint runsalembic upgrade head before serving, so a deploy always migrates first.docker compose up --build brings up Postgres + the API.

Health & correlation

  • /health — liveness (no DB). /health/ready — readiness (DB ping).
  • Every request carries x-request-id, echoed into structured JSON logs — grep by it when triaging.

Common incidents

  • Boot fails in prod — check the config guard message (weak secret / SQLite URL / wildcard CORS).
  • 429s spiking — the limiter is per-process; behind multiple workers the effective limit multiplies. Move to a shared store before relying on exact limits.
  • Verification returns tampered — either genuine tampering or a canonical-serialization change. Never alter _canonical_content without a migration plan; it breaks every historical commitment.

Known gaps & roadmap

  • InvGuard v2 — replace rule/graph heuristics with a trained GNN + SHAP explainability; add Neo4j relationship analysis and LLM field extraction.
  • rify-AI v2 — real IPFS pinning + on-chain anchoring (testnet → mainnet), multi-sig/HSM signing.
  • Scale — shared-store rate limiting (Redis); streaming audit export (current report endpoints read up to 200 decisions).
  • Multi-tenancy — orgs, RBAC, billing (MVP is single-tenant).

Every gap here is a deliberate, documented scope call — not an accident. Keep it that way.