---
name: server-marchat
description: >-
  Implements marchat server hub, WebSocket handlers, admin web, health, and
  startup validation. Use when editing server/, cmd/server/, hub routing, admin
  panel, or server configuration.
paths:
  - "server/**"
  - "cmd/server/**"
  - "config/**"
---

# Server (marchat)

App entry: `cmd/server/main.go`. Library: `server/` (hub, client, handlers, db, admin, health).

## Startup

- Validate before serve: at least one admin, non-empty admin key, valid listen port.
- Admin names: trim, lowercase, case-insensitive dedupe.

## Hub and WebSocket

- Per-channel routing, DMs, typing, read receipts, reactions.
- Moderation: permanent bans and unexpired temp kicks load from `ban_history` on hub start (latest open row per user); writers close open rows before insert and persist before updating in-memory maps. See Hub mutex rules comment on `Hub` in `hub.go`.
- Inbound WebSocket messages: `readPump` rate-limits then `dispatchInbound` / typed handlers in `client_dispatch.go`.
- Outbound client messages are channel-stamped from hub membership (`stampClientChannel`); client-supplied `channel` values are ignored for routing.
- All outbound/persist paths stamp `sender` from the authenticated session (`stampSenderTimedOutbound`); NUL bytes in persistable `content` are rejected before insert; empty or whitespace-only plaintext on `text` / `dm` / `edit` is rejected when `encrypted` is false (encrypted opaque ciphertext is never treated as empty).
- Chat `content` cap: `MARCHAT_MAX_MESSAGE_BYTES` / `MARCHAT_MAX_MESSAGE_MB` (default 32 KiB) checked in `dispatchInbound` for non-file types (System reply, connection stays open, no persist/broadcast). `SetReadLimit` stays the file DoS ceiling. Plugin chat and command replies over the cap are dropped in `Hub.Run` before broadcast (`SetMaxMessageBytes` before `Run`).
- Reserved usernames during handshake (no double-book before registration).
- Serialized writes per connection (`client.go`).
- File uploads: `SetReadLimit` uses `websocketReadLimit` (max of policy `fileMessageReadLimit` wire size and a **32 MiB** DoS ceiling) so modest oversize is fully read; declared/payload checks send a System reply and `continue`. `ErrReadLimit` (above the ceiling) logs rejection only - gorilla already sent empty close **1009**, so a System enqueue cannot flush.
- Read-pump rate limits: constants shared with `loadverify_ratelimit_test.go`.
- Handshake replay: up to 50 **visible** recent messages (SQL limit after DM/public filter).

## Admin

- TUI: `admin_panel.go`, `config_ui.go` (Charm v2: `tea.View`, `KeyPressMsg`, bubbles setters). Admin panel enables `MouseModeCellMotion` and routes `MouseWheelMsg` for scrollable tabs and user/plugin tables. Kick/ban cmds claim success only when hub `KickUser`/`BanUser` return nil (self-target, not-connected kick, and permanently-banned kick errors map to failed action messages). `KickUser` is online-only; offline kicks return `ErrKickNotConnected`.
- Web: `admin_web.go`, `admin_web.html`; `MARCHAT_SESSION_SECRET` (preferred), `MARCHAT_JWT_SECRET` deprecated; CSRF on mutating routes; login rate limit per IP. User kick/ban actions return `success: false` with a message when the hub rejects the target.
- Trusted proxies: `MARCHAT_TRUSTED_PROXIES` for forwarded client IP.

## Security

- Origin checks on parsed hostnames; optional `MARCHAT_ALLOWED_ORIGINS`.
- Never log session secrets or admin keys.
- E2E payloads opaque at rest and in logs.

## Backup

- `:backup` and admin backup actions: SQLite only (`BackupDatabase` checks dialect). Postgres/MySQL return a clear error; use native tools for those backends.

## Config

- Root `config/` package: env + `config/.env` (`godotenv.Overload`).
- `MARCHAT_DB_PATH` for database backend (`database-marchat` skill).

## Testing

- `handlers_test.go`, `hub_test.go`, `integration_test.go`, `admin_web_test.go`.
- Subprocess doctor: `cmd/server/subprocess_doctor_test.go`.

## Health

Metrics and health HTTP: `server/health.go`.
