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 appLocal 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/docsDefaults 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:3000The 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 idxInvariant: 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 cleanrify-AI internals
The trust layer has one hard requirement: reproducibility. The steps:
- Canonical content — a JSON serialization of the committed fields with
sort_keys=Trueand compact separators. Neverstr(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_LOGSThe 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.tswithcache: no-storeanddynamic = "force-dynamic". - Route Handlers under
app/api/*proxy interactive writes/verify, injectingBACKEND_API_KEYserver-side. - Client Components (DecisionsWorkbench) call only same-origin
/api/*— no CORS, no secrets in the bundle. lib/api.tsimportsserver-onlyso 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 checkDeployment & 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_contentwithout 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.