Architecture
Monorepo layout, runtime apps, and domain/db/integration layering.
pnpm workspaces + Turborepo. Apps orchestrate; shared packages own rules, persistence, and adapters.
Workspace map
Caption: apps depend on shared packages; the docs site consumes the committed OpenAPI artifact via symlink.
Layering rules
packages/domain— pure TypeScript; no I/O. Permission decisions only viacan().packages/db— Drizzle / PostgreSQL; repositories and migrations.packages/integrations— interfaces + mock/sandbox/real adapters.- Apps stay thin: route handlers and UI orchestrate the packages above.
For the executable boundaries behind this summary, continue with:
- Application architecture — transport adapters, typed command/query outcomes,
can(), transactions, audit, repositories and providers. - Tenant isolation and fleet lifecycle — route/validate separation, physical database isolation, provisioning and fleet migrations.
- Workers and integrations — job registry, queue delivery, adapter tiers, retries and idempotency.
- Operations and runbooks — purpose-based landing page for every canonical procedure.
Runtime model
apps/web/apps/portal— Next.js App Router.apps/workers— Node process draining BullMQ on Valkey.apps/control-plane—blinqx-controlCLI for provision and fleet migrations.- Local backing services: Postgres, Valkey, OpenSearch, MinIO, Keycloak via
docker/compose.yml.
Permission engine and scope filtering
Caption: can() in packages/domain is the single decision path for roles, department scope, and record access level, returning a named rule for explainability. Bulk reads share the same logic through compileToFilter() (ADR-0002), which compiles the policy into a Drizzle predicate or an OpenSearch filter instead of post-filtering fetched rows. departmentSplit can’t be pushed into a query, so a permanent per-row can() re-check runs on whatever the compiled filter returns — fail-closed by design, not a migration shim.
Offline sync (desktop client)
Caption: the Tauri client (offline-sync/) owns no conflict or deletion logic — it only scans and executes. The server runs one shared reconcile decision table (reconcileDeviceSync in packages/domain/src/device-sync, ADR-0014) so device sync, SharePoint/OneDrive sync (ADR-0013), and permission checks stay in a single place instead of forking per client.
AI anonymize pipeline
Caption: packages/ai’s anonymizer is deterministic code, not a model — it masks Dutch identifiers (BSN, IBAN, KvK, phone, address, names) with stable per-value tokens before anything leaves the tenant, and asserts no raw identifier remains before calling ModelGateway. The response is re-identified inside the tenant boundary and every invocation is audited; AI proposes drafts, it never persists (CLAUDE.md rule 5).
Billing to Peppol/UBL
Caption: sendInvoiceAction (apps/web/src/lib/actions/billing.ts) picks a channel — pdf, ubl, peppol, or ledes — and for peppol resolves a PeppolAccessPoint (mock or Storecove, packages/integrations/src/peppol). packages/domain/src/billing assembles the invoice and renders UBL XML via toUbl() before handing it to the access point for delivery; delivery status round-trips back onto the invoice record.
Email filing
Caption: the Outlook add-in exchanges a verified Office SSO token for a five-minute, Outlook-filing-only Basenet bearer; the OBO Graph token stays server-side. The existing personal-API-token bearer remains a fallback. The taskpane sends the selected message and attachments to the real file-to-matter route, which applies the shared permission engine and DMS repository. Filing is the boundary between the provider-backed mailbox read model and the active Documenthub: it produces an immutable DMS document plus email_link, and provider-side deletion can never remove the filed copy.
Source anchors
- Root
README.md,AGENTS.md,pnpm-workspace.yaml,turbo.json - OpenWiki (full generated tree):
openwiki/architecture/
See also: Application architecture · Tenant isolation · Workers and integrations · Operations · Public API