# Shared Bouncer Architecture Design

Date: 2026-03-07

## Overview

Free-tier users share a single soju bouncer instance (`soju-shared`). Pro-tier users
get dedicated bouncer instances with a gamja web chat sidecar. A validation webhook
enforces plan-based quotas. Migration between shared and dedicated is user-initiated
and bidirectional.

## Architecture

```
                    free user                          pro user
                        |                                  |
                  chat.irc.now                    apple.irc.now (443)
                  (gamja + nginx)                 (gamja sidecar)
                        |                                  |
                    WebSocket                          WebSocket
                        |                                  |
                 soju-shared:8080                   apple:8080
              (multi-user bouncer)              (dedicated bouncer)
                        |                                  |
                 soju_shared DB                     soju_apple DB
              (all free user data)              (single user data)
```

### soju-shared

A SojuBouncer CR deployed via `deploy/instances/soju-shared.yaml`. Serves all free-tier
users with soju's built-in multi-user isolation (`enable-user-on-auth true`). Each user
authenticates via SASL OAUTHBEARER and gets their own networks, channels, and message
history in the shared tenant database. No Route -- only reachable internally via the
chat deployment.

### Dedicated Bouncers

Existing SojuBouncer CRs, one per pro user. The soju-operator adds a gamja sidecar
container to the Deployment and creates an edge TLS Route at `{name}.irc.now:443` for
web chat. The existing passthrough Route handles IRC on port 6697. Same hostname,
different ports.

### chat.irc.now

Unchanged. gamja + nginx proxying WebSocket to `soju-shared:8080`. Free-tier web chat
entry point.

## Plan Tiers

| Feature              | Free ($0)           | Pro ($4/mo)              |
|----------------------|---------------------|--------------------------|
| Bouncer              | Shared              | Dedicated                |
| Upstream networks    | 1                   | Unlimited                |
| Scrollback           | 48h                 | Unlimited                |
| Custom hostname      | No                  | Yes                      |
| Web chat             | chat.irc.now        | {name}.irc.now           |
| ErgoNetwork creation | No                  | Yes                      |

## Quota Enforcement

### Portal level (web-api)

- `POST /bouncers`: rejected if `plan == "free"`. Free users use soju-shared
  automatically.
- Dashboard UI: free users see "Shared Bouncer" section with a link to chat.irc.now
  and an upgrade CTA. Pro users see bouncer management with create/delete.
- Network management for free users: portal checks network count before allowing
  addition. Limited to 1 upstream network.

### Validation webhook (bouncer-webhook crate)

A new crate deployed as a Service, registered as a `ValidatingWebhookConfiguration`
on `SojuBouncer` CREATE operations.

The webhook:
1. Extracts the `irc.now/owner` label from the incoming CR
2. Looks up the user's plan from the accounts DB
3. Rejects creation if plan is `free`
4. For pro users, enforces max bouncer count (1 dedicated bouncer)

This is the safety net -- even if someone bypasses the portal and applies a CR
directly, the webhook blocks it.

### Soju-level enforcement

Free-tier network limit (1 upstream network) enforced at both the portal level and
via soju configuration where possible. The portal talks to soju's admin socket to
manage networks for shared bouncer users.

## Gamja Sidecar (Dedicated Bouncers)

When the soju-operator reconciles a dedicated bouncer (any SojuBouncer CR that is
not `soju-shared`), it adds:

### Sidecar container

- Image: same chat image (gamja + nginx)
- nginx config generated by the operator, proxying `/socket` to `localhost:8080`
  (soju in the same pod)
- Port 8081 for web traffic (avoids conflict with soju's 8080 WebSocket)
- gamja `config.json` injected via ConfigMap with the bouncer's OAuth2 settings

### Routing

- Passthrough Route: `{name}.irc.now:6697` for IRC (already exists)
- Edge Route: `{name}.irc.now:443` for web chat -> gamja sidecar on 8081
- Same hostname, different ports, different TLS modes

## Migration

### Free to Pro (upgrade)

User-initiated. Upgrading to pro unlocks the bouncer management UI but does not
automatically create a dedicated bouncer.

1. User upgrades via Stripe. Portal shows bouncer management with "Create Bouncer".
2. User creates a bouncer. The create form has a checkbox: "Migrate data from shared
   bouncer".
3. If unchecked: dedicated bouncer created fresh.
4. If checked: dedicated bouncer created, then a Kubernetes Job migrates user data
   from `soju_shared` DB to the new tenant DB.

Migration Job copies:
- User record
- Network configurations
- Channel join list
- Delivery receipts
- WebPush subscriptions

Message logs are NOT migrated (scrollback resets).

The Job removes the user from `soju_shared` DB after successful copy. If the Job
fails, the dedicated bouncer CR is deleted and the user stays on shared. Portal shows
an error.

### Pro to Free (downgrade)

On subscription cancellation or user-initiated bouncer deletion:

1. Portal shows "Migrate to Shared Bouncer" with a network picker if the user has
   more than 1 upstream network (free tier allows 1).
2. Reverse migration Job copies selected network + channels + receipts back to
   `soju_shared` DB, within free-tier quota limits.
3. Excess networks and their data are dropped.
4. Message logs not migrated (48h scrollback resets).
5. Dedicated bouncer CR is deleted (operator deprovisions tenant DB).

## Components to Build

1. **Deploy soju-shared** -- apply `deploy/instances/soju-shared.yaml`, verify
   chat.irc.now connects to it
2. **Soju-operator: gamja sidecar** -- add gamja container + edge Route generation
   for dedicated bouncers
3. **Web-api: plan gating** -- free users see shared bouncer UI, pro users see
   bouncer management. Block bouncer creation for free tier.
4. **Web-api: migration UI** -- "Migrate from shared" checkbox on create,
   "Migrate to shared" on delete/downgrade with network picker
5. **Migration Job** -- copies user data between soju tenant DBs, respects quotas
   on reverse migration
6. **Validation webhook (bouncer-webhook)** -- new crate, ValidatingWebhookConfiguration.
   Enforces: no bouncer creation for free, max bouncer count for pro, network limits.
7. **Network limit enforcement** -- portal + soju-level checks for free-tier
   1-network limit
