Skip to content

Architecture ​

Rails, following Basecamp's conventions closely: thin controllers, fat models, concerns over inheritance, Hotwire for interactivity, and no service-object layer for its own sake.

Layout ​

app/
  controllers/         thin — set ivars, call models, authorise
    api/v1/            the JSON and Markdown API
    concerns/          authentication, request context
  models/              domain logic
    concerns/          cross-model behaviour
    <model>/           model-specific concerns, e.g. entity/check_posture.rb
  lib/
    risk/              the risk engine: assessment, gates, tiers, provenance, observations
    providers/         one namespace per provider, adapters and clients
    provider_fetch_runs/  config resolution, projection, payload sanitising
    person_resolution/    identity linking and deduplication
    screening/            disposition resolution
  policies/            Pundit policies, one per resource
  views/               ERB, Turbo Streams, jbuilder for the API
  jobs/                Solid Queue

Key seams ​

Risk::Assessment — the only writer of entities.risk_level, and it always writes risk_level_updated_at with it.

Risk::Tier — the single score-to-band function, used for the overall score and for individual factor values, which is what makes per-factor labels and the overall band agree by construction rather than by two sets of thresholds drifting apart.

ProviderFetchRuns::ConfigResolver — the one place that decides which provider, identifier and adapter serve a given check.

ProviderFetchRun.capture! — the one place a provider call is recorded, success or failure.

Screening::Disposition — the one resolver the approval guard and the screening panel share, so what the panel shows and what the guard enforces cannot disagree.

Policies — every action authorised through Pundit, with account scoping in the policy scope rather than trusted from params.

Multi-tenancy ​

Everything is scoped to an account. Policy scopes filter by account_id; API lookups go through the policy scope so a cross-account record is not found rather than forbidden.

Request-scoped state ​

Current carries the session, user, account and request details. Nothing passes current_user through layers.

Frontend ​

Importmap, Propshaft, Turbo and Stimulus. No bundler, no Node in the app's asset path. Interactivity is Turbo Frames and Streams; Stimulus is reserved for genuinely browser-side behaviour.

The design system lives in app/assets/stylesheets/verity.css with one rule worth repeating: strong colour means risk, never decoration and never a count. A number nobody measured is rendered in slate.

Background work ​

Solid Queue for jobs, Solid Cache and Solid Cable on their own databases. Asynchronous checks (IBAN verification) and webhook deliveries run as jobs.