Persist chat history to Postgres via durable JetStream consumer

DATABASE_URL required at startup; schema self-initializes.
Compose gains a postgres service for standalone dev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 22:14:26 +02:00
parent 6b97ec3b0f
commit ec3771f2e7
10 changed files with 696 additions and 21 deletions
+17 -5
View File
@@ -8,6 +8,8 @@ Axum) where every room is a **NATS subject** and sign-in is **Kanidm SSO**
browser ──(server fn POST)──▶ axum ──publish──▶ NATS chat.room.<room>
browser ◀──(SSE /sse/<room>)── axum ◀─subscribe── NATS chat.room.<room>
browser ◀──(302 /auth/*)────── axum ◀──OIDC──▶ Kanidm
axum ◀─JetStream (CHAT / cnats-postgres)─ NATS
└──INSERT──▶ Postgres (history archive)
```
The browser never talks to NATS directly: the server publishes on behalf of the
@@ -61,6 +63,7 @@ cp .env.example .env # then edit
| Variable | Meaning |
| --- | --- |
| `NATS_URL` | NATS server, default `nats://127.0.0.1:4222` |
| `DATABASE_URL` | Postgres for message history, default `postgres://cnats:cnats@127.0.0.1:5432/cnats` |
| `KANIDM_URL` | Base URL of Kanidm, e.g. `https://idm.example.com` |
| `OAUTH2_CLIENT_ID` | The Kanidm oauth2 client name (`cnats` above) |
| `OAUTH2_CLIENT_SECRET` | Output of `show-basic-secret` |
@@ -70,18 +73,23 @@ cp .env.example .env # then edit
## Run
```bash
docker compose up -d nats
docker compose up -d nats postgres
cargo leptos watch # dev, auto-reload on http://127.0.0.1:3000
cargo leptos build --release # production build (binary + target/site)
```
Or run the whole stack (app image included) with `docker compose up`.
Open two browser windows, sign in, and chat — or join from the CLI:
```bash
nats sub 'chat.room.*'
nats pub chat.room.lobby '{"id":"cli-1","room":"lobby","username":"cli","display_name":"CLI","text":"hello from the bus","time":"12:00:00"}'
nats pub chat.room.lobby '{"id":"cli-1","room":"lobby","username":"cli","display_name":"CLI","text":"hello from the bus","time":"12:00:00","ts":0}'
```
CLI-published messages are archived too — the JetStream consumer sees
everything on `chat.room.*`, not just what this app publishes.
## Layout
```
@@ -94,6 +102,7 @@ src/
server/
oidc.rs Kanidm OIDC login/callback/logout handlers
sse.rs NATS → browser SSE bridge (one subscription per client)
store.rs JetStream → Postgres archive + room history queries
style/main.css the console theme
```
@@ -102,9 +111,12 @@ style/main.css the console theme
- Sessions are in-memory (`tower-sessions` `MemoryStore`): restart logs
everyone out, and it is single-instance. Swap in a Redis/SQL store for
multiple replicas.
- Messages are not persisted; you only see traffic while connected. NATS
JetStream (the compose file starts NATS with `-js`) would give history via a
durable stream over `chat.room.*`.
- History: a durable JetStream pull consumer (`CHAT` stream, `cnats-postgres`
consumer) archives `chat.room.*` into Postgres; joining a room backfills the
last 100 messages. Inserts are idempotent on message id (JetStream is
at-least-once). The `CHAT` stream keeps default limits — cap it with
`nats stream edit CHAT --max-age=…` if the bus is chatty, since Postgres
already holds the archive.
- The OIDC client verifies ID-token signature, nonce, and CSRF state, and
requires PKCE — but there is no token refresh; the app session lives
independently of the Kanidm token lifetime.