# Dezky — Local Development Environment Setup

> **For Claude Code:** This is a complete handover document for spinning up the Dezky platform locally for development. Everything you need to know is here.

## Mission for this session

The user (Ronni) has already:
- Selected brand: **Dezky** (dezky.com pending purchase)
- Run `mkcert` on macOS to generate local TLS certificates
- Decided on production target: Hetzner AX41-NVMe + Object Storage + Storage Box
- Defined the full stack (see Stack section)

Your job: Get the entire Dezky platform running locally via Docker Compose so Ronni can develop against it. The user has solid technical fluency (NestJS, Nuxt, MongoDB, Docker, Kubernetes) — skip basic explanations, but be thorough on Dezky-specific configuration.

## Stack — what we're spinning up

All components are Apache 2.0 / MIT licensed for clean commercial multi-tenant hosting.

| Service | Image | Purpose | Local URL |
|---------|-------|---------|-----------|
| Traefik | `traefik:v3.2` | Reverse proxy + TLS termination | `https://traefik.dezky.local:8443` |
| PostgreSQL | `postgres:16-alpine` | Shared RDBMS for Authentik, OCIS | (internal) |
| MongoDB | `mongo:7` | Portal application data | (internal) |
| Redis | `redis:7-alpine` | Cache + session store | (internal) |
| Authentik | `ghcr.io/goauthentik/server:2025.10` | Identity provider, OIDC/SAML | `https://auth.dezky.local` |
| Stalwart Mail | `stalwartlabs/mail-server:latest` | Mail (SMTP/IMAP/JMAP/CalDAV) | `https://mail.dezky.local` |
| OCIS | `owncloud/ocis:7.0` | File storage (S3-compatible backend) | `https://files.dezky.local` |
| Collabora | `collabora/code:latest` | Office document editor inside OCIS | `https://office.dezky.local` |
| Portal stub | (built from `./apps/portal`) | Nuxt 3 customer portal | `https://app.dezky.local` |
| Platform API | (built from `./services/platform-api`) | NestJS service · tenants/partners/users/provisioning orchestration | `api.dezky.local` (+ internal port 3001) |

**NOT included in this dev setup** (added in later phases):
- Jitsi Meet (4-5 sub-containers — see `docker-compose.optional.yml` when ready)
- Zulip (resource-heavy — added separately when chat features are needed)

## Critical setup invariants

1. **All code in English** — variable names, comments, schema fields, routes, function names. UI strings may be Danish for end users only.
2. **TypeScript everywhere** in apps and services.
3. **mkcert TLS certs** — the user has already run `mkcert -install` and generated wildcard certs. They will be placed in `infrastructure/docker-compose/certs/`.
4. **All hostnames use `.dezky.local`** in development. `/etc/hosts` entries required (see `scripts/setup-hosts.sh`).
5. **PostgreSQL is shared** among Authentik and OCIS. Each gets its own database and user (see `configs/postgres/init.sql`).
6. **MongoDB is dedicated** to the portal application data (matches user's existing stack pattern: Målerportal, TurtleLootLine).
7. **Secrets are .env-based** for dev — production will use SOPS/sealed-secrets in k3s.

## Expected user environment

- macOS (uses `brew` for installs)
- Docker Desktop or OrbStack running
- mkcert installed and root CA trusted
- 16+ GB RAM (stack uses ~10 GB at peak)
- `pnpm` for Node workspaces
- `git` for version control

If anything is missing, instruct the user with the exact `brew install` command.

## File structure

```
dezky/
├── CLAUDE.md                              # This file
├── README.md                              # Human-facing setup guide
├── .env.example                           # All required env vars
├── .gitignore
├── apps/
│   └── portal/                            # Nuxt 3 portal (stub for now)
├── services/
│   └── platform-api/                      # NestJS service · platform control plane
├── packages/                              # Shared TypeScript packages (empty for now)
├── infrastructure/
│   └── docker-compose/
│       ├── docker-compose.yml             # Main stack
│       ├── docker-compose.optional.yml    # Jitsi + Zulip (later)
│       ├── certs/                         # mkcert TLS certs go here
│       │   ├── dezky.local.pem            # User places mkcert output here
│       │   └── dezky.local-key.pem
│       └── configs/
│           ├── traefik/
│           │   ├── traefik.yml            # Static config
│           │   └── dynamic.yml            # TLS + middleware
│           ├── stalwart/
│           │   └── config.toml            # Mail server config
│           ├── postgres/
│           │   └── init.sql               # Create DBs/users
│           ├── authentik/
│           │   └── blueprints/            # (Optional) Pre-configured flows
│           └── ocis/
│               └── (config files mounted at runtime)
├── scripts/
│   ├── bootstrap.sh                       # One-command setup
│   ├── setup-hosts.sh                     # /etc/hosts entries
│   ├── setup-certs.sh                     # Copy mkcert certs to right place
│   └── reset.sh                           # Nuke and start fresh
└── docs/
    ├── SERVICES.md                        # Per-service reference
    ├── AUTHENTIK-SETUP.md                 # First-time Authentik walkthrough
    ├── TROUBLESHOOTING.md                 # Common issues
    └── NEXT-STEPS.md                      # What to do after setup works
```

## Sequence for Claude Code to execute

When the user runs `claude` in the project directory, walk through:

### Phase 1: Verify environment (5 min)

```bash
# Check that required tools are installed
docker --version       # Need 24.0+
docker compose version # Need v2
mkcert -version        # Need 1.4+
pnpm --version         # Need 9+
node --version         # Need 20+
```

If anything is missing, halt and instruct user with brew install commands.

### Phase 2: Cert setup (2 min)

The user has already run `mkcert -install`. We need to:

1. Check if `infrastructure/docker-compose/certs/dezky.local.pem` exists
2. If not, run mkcert for the wildcard domain:
   ```bash
   cd infrastructure/docker-compose/certs
   mkcert "*.dezky.local" "dezky.local" "localhost" "127.0.0.1" "::1"
   mv ./_wildcard.dezky.local+4.pem dezky.local.pem
   mv ./_wildcard.dezky.local+4-key.pem dezky.local-key.pem
   ```

### Phase 3: DNS setup (2 min)

Run `scripts/setup-hosts.sh` which adds these to `/etc/hosts` (requires sudo):

```
127.0.0.1  dezky.local app.dezky.local auth.dezky.local
127.0.0.1  mail.dezky.local files.dezky.local meet.dezky.local
127.0.0.1  chat.dezky.local office.dezky.local
127.0.0.1  traefik.dezky.local
```

### Phase 4: Environment variables (2 min)

Copy `.env.example` to `.env` and generate secure random values:

```bash
cp .env.example .env
# Generate 64-char hex for each *_SECRET / *_KEY
openssl rand -hex 32  # run multiple times, paste into .env
```

### Phase 5: First boot (10 min)

```bash
cd infrastructure/docker-compose
docker compose pull           # Get all images first
docker compose up -d postgres redis mongo
# Wait for healthchecks
docker compose logs -f postgres
# When ready, start identity layer
docker compose up -d authentik-server authentik-worker
# Wait for Authentik to be ready
docker compose logs -f authentik-server
# Then start everything else
docker compose up -d
```

### Phase 6: Authentik initial setup (15 min)

Open `https://auth.dezky.local` and walk through:
1. Create admin user (akadmin)
2. Configure OIDC providers for: ocis, portal, stalwart
3. Test SSO end-to-end

See `docs/AUTHENTIK-SETUP.md` for the exact steps.

### Phase 7: Verify it all works (5 min)

- `https://app.dezky.local` — Portal landing page should load
- `https://auth.dezky.local` — Authentik login screen
- `https://files.dezky.local` — OCIS should redirect to Authentik for SSO
- `https://mail.dezky.local` — Stalwart admin UI

## Important context for working on this project

### Stack rationale (DO NOT suggest replacing components)

These choices were made deliberately after extensive license/architecture research:

- **Stalwart over Mailcow**: Modern Rust, ActiveSync built-in, JMAP support, single binary
- **OCIS over Nextcloud**: Apache 2.0 vs AGPL+trademark fees for whitelabel
- **Zulip over Element/Mattermost/Rocket.Chat**: Only truly open-core-free chat option
- **Authentik over Keycloak**: Better multi-tenancy, MIT license, simpler config
- **Hetzner over AWS/GCP**: 100% EU sovereignty pitch — this is core to the business

### Code conventions

- **TypeScript strict mode** in apps/services
- **English only** in all code identifiers, comments, schema fields
- **Conventional Commits** for git messages: `feat:`, `fix:`, `chore:`, `docs:`
- **Prefer prose comments** over heavy JSDoc — explain *why*, not *what*
- **MongoDB** for portal app data (consistent with Målerportal, TurtleLootLine)
- **PostgreSQL** for services that require it (Authentik, OCIS)
- **Feature flags ship through `useFeatureFlag('key')`**, NOT hardcoded
  `if (env === ...)` checks. Risky / plan-gated / kill-switchable features
  go behind a flag. See [`docs/FEATURE-FLAGS.md`](./docs/FEATURE-FLAGS.md)
  for when to add one, how to use the composable, and the 4 states / 4 scope
  axes.

### Production target (for reference, not deploy now)

Eventually moves to single Hetzner AX41-NVMe (€39/mo) with:
- Stalwart on bare-metal (not Docker)
- k3s for all other services
- Hetzner Object Storage (€5/mo) for OCIS S3 backend
- Storage Box BX11 (€3.20/mo) for Restic backups
- Storage Box BX11 in Helsinki (€3.20/mo) for DR

But locally, everything runs in Docker Compose with mkcert TLS.

## What to do if stuck

- **TLS not working**: Verify `mkcert -CAROOT` matches what Docker has access to. On macOS, the root CA must be trusted in the system keychain.
- **Authentik won't start**: PostgreSQL needs to be fully ready first. Check `docker compose logs postgres` for `database system is ready to accept connections`.
- **OCIS OIDC fails**: Authentik issuer URL must be reachable from inside the OCIS container. Use the Docker network hostname, not the public URL.
- **Stalwart port 25 conflicts**: macOS may have postfix running. Disable with `sudo launchctl unload /System/Library/LaunchDaemons/org.postfix.master.plist`.
- **Cert errors in browser**: Make sure `mkcert -install` has been run AND browser has been restarted.

See `docs/TROUBLESHOOTING.md` for detailed solutions.

## After local dev works

1. Build out the Nuxt portal (`apps/portal`) — start with auth flow via Authentik OIDC
2. Build the platform API (`services/platform-api`) — first endpoint: create tenant
3. Wire portal → platform-api → Authentik/OCIS/Stalwart admin APIs
4. Add Zulip + Jitsi when ready (`docker-compose.optional.yml`)
5. When portal MVP is solid → migrate to Hetzner AX41 production

---

**Last updated:** 2026-05-23
