# irc.now Project Memory

## Project Overview
IRC bouncer-as-a-service platform. Rust workspace monorepo at ~/development/irc-now.

## Architecture
- **Workspace crates**: common, web-api, web-landing, bot-landing, net-landing, bot, soju-operator, ergo-operator, txt, pics, platform-operator, bouncer-webhook
- **Non-crate services**: chat/ (gamja + nginx, not Rust)
- **Rust**: Axum 0.8, kube.rs 3.0, sqlx, openid 0.23, askama 0.15
- **MSRV**: 1.88 (askama 0.15, kube 3.0, home 0.5.12 require it)
- **Containerfiles**: rust:1.88 -> ubi9-minimal multi-stage builds

## Cluster State (OCP 4.20)
See [cluster.md](cluster.md) for details.

### Namespaces
- `irc-josie-cloud`: web-landing, web-api, bot-landing, accounts-db, soju, soju-operator, ergo-operator, platform-operator, irc-now-net, txt, pics, minio, paste-db, pics-db, bot-db, chat
- `irc-keycloak`: keycloak-db (CNPG only, Keycloak deployment moved to `keycloak` namespace)
- `keycloak`: keycloak-operator, irc-now Keycloak CR, josie.cloud Keycloak CR

### Services Live
- `irc.now` -- landing page (web-landing)
- `irc.bot` -- bot service + landing page (bot, bot-landing)
- `network.irc.now` -- network landing page (net-landing), brand is `net.irc.now`
- `my.irc.now` -- account portal (web-api)
- `txt.irc.now` -- pastebin (txt)
- `irc.pics` -- image host (pics)
- `chat.irc.now` -- web IRC client (gamja + nginx, WebSocket proxy to soju)
- `auth.irc.now` -- Keycloak 26.1 (realm: irc-now, operator-managed CR in `keycloak` ns)
- `stats.irc.now` -- Grafana dashboards (Keycloak SSO, admin/viewer roles)

### TLS Pattern
cert-manager Certificate CR -> Secret -> Route `tls.externalCertificate.name`
ClusterIssuer: `letsencrypt-prod` (dns-01 via Cloudflare)

## Build Pattern
OCP binary builds using `--from-archive` with minimal tar (~65K):
```bash
tar czf /tmp/build.tar.gz --exclude='target' --exclude='.git' \
  --exclude='irc-now-landing-page' --exclude='status' --exclude='design' \
  --exclude='./docs' --exclude='notes' Cargo.toml Cargo.lock crates/
oc start-build <name> --from-archive=/tmp/build.tar.gz -n irc-josie-cloud --follow
```
Use `--exclude='./docs'` (not `--exclude='docs'`) to avoid excluding `crates/*/static/docs/`.

## Keycloak Theme
Custom login theme at `deploy/keycloak/theme/irc-now/login/`. Deployed via ConfigMap +
init container. To update: edit theme files, recreate ConfigMap, delete the Keycloak pod.

## Key Secrets
- `accounts-db-app` (irc-josie-cloud): CNPG-managed DB creds
- `oidc-account-portal` (irc-josie-cloud): OIDC client secret
- `oidc-paste`, `oidc-pics`, `oidc-soju`, `oidc-ergo`, `oidc-bot`, `oidc-stats` (irc-josie-cloud): per-service OIDC clients
- `stripe-keys` (irc-josie-cloud): secret-key, price-id, webhook-secret (placeholder values)
- `minio-credentials` (irc-josie-cloud): root-user, root-password
- `paste-db-app`, `pics-db-app`, `bot-db-app` (irc-josie-cloud): CNPG-managed DB creds
- `grafana-sa-token` (irc-josie-cloud): SA token for Prometheus access
- Keycloak secrets in `keycloak` + `irc-keycloak` namespaces

## CNPG Notes
- StorageClass: `customer-workload-storage` (hostPath PVs)
- SELinux context: `container_file_t` required on hostPath dirs
- UID ranges: irc-josie-cloud=1001260000, irc-keycloak=1001300000

## Phase Status
- **Phase 1** (Foundation): done. Plan: docs/plans/2026-03-03-irc-now-phase1-plan.md
- **Phase 1.5** (Fill the Gaps): done. Remaining: replace placeholder stripe-keys, Upptime, remove znc DNS
- **Phase 2** (Observability): done. Grafana, Prometheus, alertmanager, ServiceMonitors
- **Phase 3** (Supporting Services): done. txt, pics, chat, content expiry, Keycloak theme
- **Phase 4** (Network Hosting): ergo-operator done, portal integration done, wildcard DNS pending
- **Phase 5** (Analytics): done. Events table, business metrics, Grafana dashboards
- **Phase 6** (Bots): deployed. mlua Lua 5.4 sandbox, OIDC auth, PostgreSQL storage
- **Phase 7** (Shared Bouncer): code done, pending deploy. soju-shared, plan gating, gamja sidecar, bouncer-webhook, migration

## Key Patterns / Gotchas
- `stripe_util.rs` (not `stripe.rs`) to avoid shadowing the `stripe` crate
- Axum route ordering: static routes (`/my`) must precede parameterized (`/{id}`)
- SameSite::Lax required on session cookies for cross-site OIDC redirects
- `metrics-exporter-prometheus` needs `default-features = false` to avoid aws-lc-rs/ring conflict
- OnceLock<PrometheusHandle> pattern (not Extension) for metrics handler
- `make_interval` needs `$1::int` cast (sqlx binds i64 as bigint)
- CNPG user is `accounts` (not `app`) -- grants needed for new tables

## Content Expiry + Keycloak Sync
- `update_keycloak_user_attributes()` in profile.rs syncs plan + content_expires to Keycloak
- Called from: profile save, auth callback, billing webhook
- Keycloak user attributes -> protocol mappers -> ID token claims
- txt/pics: `extract_custom_claims()` base64-decodes JWT payload

## Chat Service (chat.irc.now)
gamja web IRC client served by nginx. Top-level `chat/` directory (not a Rust crate).
- Containerfile clones gamja from `git.sr.ht/~emersion/gamja` (codeberg mirror unreliable)
- OAuth2 SSO: gamja -> Keycloak (public client `chat`) -> SASL OAUTHBEARER -> soju
- Keycloak `chat` client: PKCE disabled, `exclude.session.state.from.auth.response` set, access.token.lifespan=28800 (8h)
- chat.irc.now proxies to soju-shared (free tier shared bouncer)
- Build: `oc start-build chat --from-dir=chat/ -n irc-josie-cloud --follow`

## Shared Bouncer Architecture
- Design: `docs/plans/2026-03-07-shared-bouncer-design.md`
- Plan: `docs/plans/2026-03-07-shared-bouncer-plan.md`
- soju-shared: SojuBouncer CR serving all free users, multi-user mode
- Dedicated bouncers: per-pro-user with gamja sidecar (port 8081) + edge Route
- bouncer-webhook: ValidatingWebhookConfiguration on SojuBouncer CREATE, enforces plan + quota
- Plan gating: web-api blocks bouncer/network creation for free users (403)
- Migration: `soju-operator/src/migrate.rs` -- forward/reverse user data copy between tenant DBs
- Keycloak admin creds: secret `irc-now-keycloak-admin` in `keycloak` ns (admin / from secret)

## Whois Hostname Cloaking
- CRD has `hostname_cloak: Option<String>`, operator emits `hostname-cloak` in soju.conf
- Custom soju image at `crates/soju-operator/soju/` (Codeberg master, nosqlite build)
- Cloaking applies to downstream connections only (not upstream network whois)

## Ergo Operator (net.irc.now)
- CRD: ErgoNetwork (`irc.now/v1alpha1`), manages Ergo IRC server instances
- Image: `ghcr.io/ergochat/ergo:stable`
- OAuth2 token introspection via Keycloak (`oidc-ergo` secret)
- External connect: `net.irc.now:443` TLS, internal: `irc-now-net:6667` plaintext
- Portal: network management tab at /networks (list, create, detail, delete, status polling)
- web-api ErgoNetwork CRD types in k8s.rs, `default_network_spec()` fills platform defaults
- Hostnames: `{name}.irc.now` via wildcard DNS, shared `oidc-ergo` Keycloak client
- Networks filtered by `irc.now/owner` label (same pattern as bouncers)

## Soju Version Notes
- Primary repo: Codeberg (codeberg.org/emersion/soju) -- GitHub/SourceHut are stale
- Custom image builds from Codeberg master, `nosqlite` build tag, imagePullPolicy Always

## Business Metrics + Analytics
- `events` table: signup, login, bouncer_create/delete, network_create/delete, plan changes
- `business_metrics.rs`: Stripe MRR (300s), event gauges (60s), soju stats (300s)
- txt/pics expose `GET /api/usage/{sub}` for cross-service per-user stats
- Grafana admin dashboard: MRR, conversion, churn, revenue over time

## Platform Operator (Deployed)
Manages WebService, ChatService, MinioInstance CRDs. Server-side apply, 300s reconcile.
- CRD group: `irc.now`, version: `v1alpha1`
- Deploy manifests: `crates/platform-operator/deploy/`
- Instance CRs: `deploy/instances/`

## Obsidian Docs
Service notes at ~/Documents/Obsidian Vault/98 josiedot/services/:
- irc-now-landing.md, irc-now-portal.md, irc-now-keycloak.md, irc-bot-landing.md
- irc-now-bot.md, irc-now-chat.md, irc-now-txt.md, irc-now-pics.md, irc-now-stats.md
- irc-now-net-landing.md, irc-now-soju-operator.md, irc-now-ergo-operator.md, irc-now-platform-operator.md