Skip to content
Basenet
🇳🇱 Nederlands
Esc
↑↓navigate↵open⌘Jpreview

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 via can().
  • 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:

Runtime model

  • apps/web / apps/portal — Next.js App Router.
  • apps/workers — Node process draining BullMQ on Valkey.
  • apps/control-plane — blinqx-control CLI 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

Was this page helpful?