Ronni Baslund 868a305539 feat(flags): real feature-flag system with bulk eval + operator UI
Real backend for the flags page (was pure mock). Built so it's ready for
the first risky rollout (likely the Stalwart JMAP client or the Stripe
billing engine).

services/platform-api:
- Flag schema (key, description, state, pct, scope.{plans, tenantSlugs,
  partnerSlugs, environments}, embedded history capped at 20)
- FlagsService with CRUD + evaluateAll(tenantSlug) → { key: bool }
  Eval algorithm:
    off  → false; on → true
    targeted → require non-empty scope (empty allowlist means "nobody"),
               then match every non-empty axis
    rollout  → match scope, then sha256(`${tenantId}:${key}`) % 100 < pct
  Hash-based rollout is deterministic: bumping pct only flips the new
  slice. Pure helpers (matchesScope, hasAnyScope, inRolloutBucket) are
  exported for future unit tests.
- FlagsController exposes GET /flags, GET /flags/:key, POST /flags/evaluate
  (JwtAuthGuard); POST/PATCH/DELETE require OperatorGuard. History entries
  capture the actor's email.
- SeedService idempotently creates 10 flag keys mapping to real Dezky
  concerns (jmap_native_v2, gdpr_export_v2, new_billing_engine, etc.).
  $setOnInsert so operator edits survive restarts.

apps/operator:
- 6 proxies: /api/flags index get/post, [key] get/patch/delete, evaluate post
- types/flag.ts with the shape that mirrors the backend
- pages/flags.vue: useFetch real list, row click opens FlagDetail,
  "New flag" opens NewFlagModal, scope summary column shows targeting
  at a glance
- FlagDetail.vue: side panel with segmented state, rollout slider with
  live "~N of M tenants" preview from /api/tenants, plan/tenant/env chip
  pickers, dirty-tracked Save, instant Kill-switch (PATCH state=off+pct=0),
  embedded change history
- NewFlagModal.vue: minimal create form (key + description). Everything
  else is configured in the detail panel afterward.
- CommandPalette: feature-flag rows now come from /api/flags instead of
  the dropped fixture, so newly-created flags are searchable immediately
- data/fixtures.ts: drop FLAGS / FeatureFlag exports (replaced by the
  real backend)

Smoke-tested end-to-end: list renders 10 seed flags, opening gdpr_export_v2
and flipping to rollout 25% then saving persists + adds a history entry,
kill-switch sets state=off in one click, /api/flags/evaluate returns the
correct booleans for the seeded tenant, same tenant gets the same answer
on consecutive evals (determinism), and creating + deleting a flag through
the UI roundtrips correctly.
2026-05-24 19:21:15 +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%