Ronni Baslund 02341d8ba5 feat(audit): platform-api audit log + operator UI wired to real events
Phase 1 of the audit work — capture everything we control today, ingest from
external systems (Authentik / OCIS / Stalwart) in a later phase. The mock
OP_AUDIT fixture is gone; both the /audit page and Overview's activity card
now show real events recorded by AuditService.record() in platform-api.

Schema (services/platform-api/src/schemas/audit-event.schema.ts):
  AuditEvent { at, actorType, actorId, actorEmail, actorIp, action, outcome,
    resourceType, resourceId, resourceName, tenantSlug, partnerSlug, source,
    metadata, prevHash, hash }
  Indexes: {at:-1}, {tenantSlug,at:-1}, {actorId,at:-1}, {action,at:-1}.
  prevHash/hash are nullable now; hash-chain tamper evidence is a later phase.

AuditService:
  - record() — best-effort write, swallows errors so the underlying mutation
    that succeeded isn't failed by a downstream log issue. Surfaces failures
    via Logger.
  - list() — filters: since/until/before, action (exact OR prefix match
    via leading-anchor regex), tenantSlug, partnerSlug, actorEmail, outcome,
    free-text q across action/resourceName/actorEmail/tenantSlug, limit
    (default 100, max 500). Cursor pagination via `before`.
  - No UPDATE/DELETE surface — entries are append-only by construction.

AuditController: GET /audit, behind JwtAuthGuard + OperatorGuard. No mutations
exposed; entries written internally by other modules.

X-Forwarded-For threading:
  - apps/operator/server/utils/platform-api.ts forwards the originating
    client IP to platform-api so audit entries carry a real address.
  - services/platform-api/src/auth/client-ip.ts extracts leftmost
    X-Forwarded-For, falls back to socket.remoteAddress.

Instrumented mutations (every one threads actor + IP through):
  Tenants: create, update, softDelete, setStatus(suspend/resume)
  Partners: create, update, terminate
  Flags:   create, update (incl. flag.killed verb when state=off+note=kill-switch),
           remove
  Users:   deactivate

Each controller resolves the User doc via ActorService, extracts IP via
clientIp(req), and passes { userId, email, ip } as AuditActor to the service.
FlagsService's local ActorRef collapses to AuditActor so flag history and the
audit log share one shape.

Operator UI:
  - /api/audit proxy that forwards query params verbatim
  - types/audit.ts
  - pages/audit.vue: real list with quick-pick action chips (All/Tenants/
    Partners/Flags/Users), outcome filter, free-text search, "Load older
    events" cursor pagination
  - pages/index.vue: Overview activity card swaps mock OP_AUDIT for the
    same /api/audit endpoint, rows link into /audit
  - data/fixtures.ts: OP_AUDIT / AuditEntry / AuditTone exports removed

Verified end-to-end: suspended + resumed acme, flipped oci_versioning through
rollout → kill → on, then /audit returned all 5 events with the right action
verbs (tenant.suspended, tenant.resumed, flag.updated, flag.killed,
flag.updated), actor admin@dezky.local, IP 192.168.65.1. Filters (action
prefix + free-text q) narrow correctly.

Out of scope for this commit (each gets its own conversation):
  - Authentik / OCIS / Stalwart ingest adapters (Phase 2)
  - Hash-chain tamper evidence (Phase 3)
  - TTL + cold-storage archival to Hetzner Object Storage (Phase 4)
  - GDPR right-to-erasure tooling
2026-05-24 19:50:24 +02:00

Dezky

Sovereign workspace platform for European businesses. Mail, files, calendar, video meetings — all EU-hosted, all open source.

Quick start (local development)

# 1. Clone and enter
git clone <repo-url> dezky
cd dezky

# 2. Run bootstrap (handles everything)
./scripts/bootstrap.sh

# 3. Open the portal
open https://app.dezky.local

The bootstrap script:

  • Checks prerequisites (Docker, mkcert, openssl)
  • Generates wildcard TLS certificate via mkcert
  • Adds /etc/hosts entries (with your permission)
  • Generates secure random secrets in .env
  • Pulls Docker images
  • Starts all services in correct order
  • Prints next-step instructions

Service URLs (local development)

Service URL Purpose
Portal https://app.dezky.local Customer-facing landing & launcher
Authentik https://auth.dezky.local Identity provider (OIDC/SAML)
Files (OCIS) https://files.dezky.local File storage & sharing
Mail (Stalwart) https://mail.dezky.local Mail server admin UI
Office https://office.dezky.local Collabora Online editor
Traefik https://traefik.dezky.local Reverse proxy dashboard

What's in this repo

dezky/
├── apps/portal/                Nuxt 3 customer portal
├── services/platform-api/      NestJS service · tenants, partners, users, provisioning orchestration
├── packages/                   Shared TypeScript libraries
├── infrastructure/
│   └── docker-compose/         Local development stack
├── scripts/                    Setup, reset, helpers
└── docs/                       Service references & guides

Prerequisites

  • macOS or Linux (Windows users: use WSL2)
  • Docker Desktop 24+ or OrbStack
  • mkcert (brew install mkcert)
  • pnpm 9+ (brew install pnpm)
  • Node.js 20+
  • 16 GB RAM recommended

Common commands

# Start everything
docker compose -f infrastructure/docker-compose/docker-compose.yml up -d

# View logs
docker compose -f infrastructure/docker-compose/docker-compose.yml logs -f [service]

# Stop everything (keeps data)
docker compose -f infrastructure/docker-compose/docker-compose.yml down

# Nuke and restart (DESTROYS DATA)
./scripts/reset.sh

Architecture

This is a multi-tenant SaaS platform. Each tenant gets:

  • Isolated Authentik OIDC tenant
  • Custom subdomain (e.g. customer-name.dezky.local)
  • Mail domain in Stalwart with auto-generated DKIM
  • Dedicated OCIS space hierarchy
  • Branded launcher in the portal

All components are Apache 2.0 / MIT licensed — no per-seat fees, full whitelabel rights.

Production

The production target is a single Hetzner AX41-NVMe server (€39/mo) with:

  • Stalwart on bare-metal
  • k3s for all other services
  • Hetzner Object Storage (€5/mo) as OCIS S3 backend
  • Storage Box BX11 (€3.20/mo) for Restic backups
  • Storage Box BX11 in Helsinki (€3.20/mo) for DR

See docs/PRODUCTION-DEPLOYMENT.md (TBD) for migration plan.

Stack rationale

These choices are deliberate after extensive license/architecture research. See CLAUDE.md for the full reasoning.

Component License Why this one
Stalwart Mail Apache 2.0 Modern Rust, ActiveSync built-in, JMAP support
OCIS Apache 2.0 Cleaner license than Nextcloud (AGPL+trademark)
Zulip Apache 2.0 Only truly open-core-free chat option
Authentik MIT Better multi-tenancy than Keycloak
Hetzner N/A 100% EU sovereignty — core to business

License

Application code: MIT (own code) Third-party services: see individual service licenses in stack.

S
Description
No description provided
Readme 1.1 MiB
Languages
Vue 60.4%
TypeScript 37.8%
Shell 0.9%
CSS 0.5%
JavaScript 0.4%