Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ SAMSARIX_CHAT_TOKEN_CLOCK_SKEW=30
# Persistent SQLite database location.
SAMSARIX_CHAT_DATABASE=data/samsarix-chat.db

# Guarded, unreleased v0.13 PostgreSQL preview. Comment out
# SAMSARIX_CHAT_DATABASE before selecting this backend. Prefer the URL file,
# use sslmode=verify-full outside loopback, and give every replica a unique ID.
# SAMSARIX_CHAT_STORAGE=postgres
# SAMSARIX_CHAT_POSTGRES_URL_FILE=/run/secrets/samsarix_postgres_url
# SAMSARIX_CHAT_POSTGRES_INSTANCE_ID=chat-a
# See docs/POSTGRES_PREVIEW.md for bounded pool, lease, relay, and retention settings.

# Comma-separated exact browser origins allowed for CORS and WebSockets.
# Example: http://localhost:3000,https://chat.example.com
SAMSARIX_CHAT_ALLOWED_ORIGINS=
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ on:
permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test:
strategy:
Expand All @@ -27,6 +31,7 @@ jobs:
- os: windows-latest
python: "3.12"
runs-on: ${{ matrix.os }}
timeout-minutes: 10
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
Expand All @@ -37,7 +42,7 @@ jobs:
cache: pip
- run: python -m pip install --upgrade pip setuptools wheel
- run: python -m pip install -e ".[test]"
- run: python -m pytest
- run: python -m pytest --timeout=60

postgres-foundation:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This project follows semantic versioning while it is in alpha: minor versions ma
- PostgreSQL schema v6 and internal connection-bound typing state with transition-only starts, refresh without event storms, explicit stops, database-time expiry, and bounded concurrent sweeping into durable coordination events.
- PostgreSQL schema v7 and lease-derived presence transitions with exact join/explicit-leave counts, bounded crashed-process sweeping, typing-before-leave ordering, and process-generation fencing for safe stable-ID restarts.
- PostgreSQL schema v8 realtime-retention metadata, bounded count/age pruning behind every live cursor, explicit retained-gap detection, and relay recovery that fences sockets, rotates the stale process generation, and resumes from the authoritative event head.
- Guarded PostgreSQL application configuration and lifecycle orchestration for shared HTTP storage, cross-instance WebSocket messages and room state, global socket capacity/rate controls/stats, leased connection renewal, sender-excluded presence/typing, bounded maintenance, and readiness.
- A PostgreSQL preview deployment guide covering protected URL files, mandatory remote `verify-full` TLS, unique replica identities, pool/lease/retention bounds, migration, backup/rollback ownership, and remaining release gates.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### Security and operations

Expand All @@ -32,6 +34,7 @@ This project follows semantic versioning while it is in alpha: minor versions ma
- Internal typing coordination events retain an opaque origin connection ID so future application wiring can exclude the sender. The public realtime relay does not forward these events until that exclusion contract is implemented; clients must continue treating advertised expiry as the stop-event backstop.
- A restarted process rotates its database generation token after lease expiry. Connection, typing, count, renewal, and cleanup queries require the matching generation so stale sockets cannot regain capacity, activity, or presence merely because an operator reused a stable instance name.
- Event pruning records the greatest intentionally removed sequence even when no event rows remain. A returning stale relay cannot mistake that empty window for a healthy cursor: it closes local sockets before skipping to current authoritative state, and its generation rotation makes old connection leases non-live.
- PostgreSQL replica IDs are exclusive generation-owned claims: a duplicate active owner fails closed, graceful shutdown releases the exact generation, and a stale process cannot heartbeat, read, or acknowledge after a replacement takes ownership.
- The asymmetric-authentication, test, and development dependency ranges now require `cryptography>=50,<51`, excluding the `cryptography>=44.0.0,<50.0.0` range affected by `PYSEC-2026-3552`.

## 0.12.0 — 2026-08-02
Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ python examples/02_websocket_chat.py

See [Getting started](docs/GETTING_STARTED.md) for authentication and browser examples, [Conversation controls](docs/CONVERSATION_CONTROLS.md) for moderation workflows, and [Data lifecycle operations](docs/OPERATIONS.md) for export, deletion, retention, backup, and restore.

The development branch also contains a guarded, unreleased PostgreSQL multi-instance mode. It is fully wired through the application but remains a preview until its subprocess, interruption, load/soak, backup, and rollback gates pass. See [PostgreSQL multi-instance preview](docs/POSTGRES_PREVIEW.md); SQLite remains the default and the supported v0.12 deployment.

The [application-workflow guide](docs/APPLICATION_WORKFLOWS.md) and runnable `examples/03_support_workflow.py` show a two-party support case with separate customer and agent identities. [Reliable application webhooks](docs/WEBHOOKS.md) covers receiver verification, retries, replay, rotation, and failure recovery.

## Container quick start
Expand Down Expand Up @@ -128,6 +130,9 @@ All settings are optional for loopback development. Copy [.env.example](.env.exa
| Variable | Default | Purpose |
| --- | ---: | --- |
| `SAMSARIX_CHAT_DATABASE` | `data/samsarix-chat.db` | SQLite database path |
| `SAMSARIX_CHAT_STORAGE` | `sqlite` | Storage backend; `postgres` selects the guarded v0.13 preview |
| `SAMSARIX_CHAT_POSTGRES_URL_FILE` | unset | Preferred protected PostgreSQL URL file; required with the preview unless direct URL form is used |
| `SAMSARIX_CHAT_POSTGRES_INSTANCE_ID` | unset | Required unique stable replica identity in PostgreSQL mode |
| `SAMSARIX_CHAT_API_KEY` | unset | Shared secret protecting all `/v1` data; minimum 16 characters |
| `SAMSARIX_CHAT_API_KEY_FILE` | unset | File alternative to `API_KEY`; never set both |
| `SAMSARIX_CHAT_TOKEN_SIGNING_SECRET` | unset | Enables signed application-user tokens; minimum 32 bytes |
Expand Down Expand Up @@ -200,7 +205,7 @@ HTTP / WebSocket clients
- Search is room-authorized, current-state Unicode-normalized substring matching over at most the configured retained messages for that room; it is not global, fuzzy, or externally indexed.
- Typing signals are transition-only, separately rate-limited, automatically expired, and never persisted or audited.
- WebSocket delivery and presence events are best-effort/at-most-once. Reconnecting clients recover the last 50 messages and can page older history over HTTP.
- Running multiple worker processes is not supported: each process would have an independent connection registry and rate limiter. Use one process or add a real broker in a future release.
- SQLite remains one-process only. The guarded PostgreSQL preview uses database-owned cursors, connection leases, rate buckets, typing state, presence, and an ordered event log across replicas; it is not a supported scale claim until the published acceptance gates pass.
- Retention always applies configured count caps and can additionally apply an operator-selected maximum age.

## Security, privacy, and operating cost
Expand Down Expand Up @@ -228,7 +233,7 @@ CI runs the tests on CPython 3.10–3.14 on Linux and CPython 3.12 on Windows, v

## Limitations and project status

This is a coherent single-instance MVP, not a hosted chat platform. The container and Compose profile support exactly one process and replica. The accepted [multi-instance architecture](docs/MULTI_INSTANCE_ARCHITECTURE.md) selects PostgreSQL plus a transactional event log for v0.13, but remains explicitly unreleased until its cross-process failure gates pass. Load/soak testing and attachments with explicit storage policy follow. Those are intentionally not presented as current capabilities.
This is a coherent single-instance MVP, not a hosted chat platform. The container and Compose profile support exactly one SQLite process and replica. The guarded [PostgreSQL preview](docs/POSTGRES_PREVIEW.md) now wires the accepted [multi-instance architecture](docs/MULTI_INSTANCE_ARCHITECTURE.md) through real application instances, but remains explicitly unreleased until its remaining process-failure and measured-load gates pass. Attachments with explicit storage policy follow. Those are intentionally not presented as current supported capabilities.

## License

Expand Down
8 changes: 6 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,13 @@ This makes the supported topology repeatable without implying that a container m
- [x] implement connection-bound PostgreSQL typing transitions, refresh, and bounded expiry sweeping;
- [x] derive join/leave presence from connection leases with generation-fenced restart and crash convergence;
- [x] bound the retained event log behind live cursors and fence/recover stale workers that return after a gap;
- [ ] expose guarded PostgreSQL configuration only after the remaining topology gates are proven;
- [ ] wire the proven cross-worker fan-out, presence, typing, and rate-control paths into the application;
- [x] expose explicitly guarded PostgreSQL preview configuration without changing the SQLite default;
- [x] wire cross-instance fan-out, presence, typing, rate controls, connection leases, maintenance, and readiness into the application;
- [ ] prove subprocess kill/restart, listener interruption, archive/ban teardown, and reconnect recovery against real network processes;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- [ ] run sustained load/soak and reconnect-storm tests and publish measured limits;
- [ ] verify deployment manifests assign a unique stable instance ID to every replica and reject duplicate live ownership;
- [ ] validate live-lag and `NOTIFY` interruption behavior under measured traffic;
- [ ] exercise and publish PostgreSQL-native backup, point-in-time recovery, restore, and application rollback evidence;
- [ ] add OpenTelemetry hooks only when an operator needs them, with telemetry disabled by default.

No horizontal-scale claim is acceptable before those tests pass. Redis Pub/Sub is at-most-once and does not solve shared storage, migration/restore coordination, webhook leadership, or distributed quotas. A broker and shared authoritative database must solve a demonstrated topology together rather than decorate the architecture.
Expand Down
8 changes: 4 additions & 4 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Returns `200 {"status":"ok"}` when the process can serve requests.

### `GET /readyz`

Returns `200 {"status":"ready"}` when SQLite answers a query, or `503 {"status":"not_ready"}`.
Returns `200 {"status":"ready"}` when the configured storage backend answers a query, or `503 {"status":"not_ready"}`. SQLite is the supported v0.12 backend; the guarded v0.13 PostgreSQL preview applies the same public response contract.

### `POST /v1/rooms`

Expand Down Expand Up @@ -148,7 +148,7 @@ A mute preserves reads and blocks writes. A ban blocks HTTP room reads/writes, r

### `GET /v1/stats`

Returns the current process's active WebSocket connection count.
Returns the current process's active WebSocket connection count in SQLite mode. In the guarded PostgreSQL preview, the count is deployment-wide across active instance leases.

### `GET /v1/admin/audit-events?limit=50&before={event_id}`

Expand Down Expand Up @@ -257,8 +257,8 @@ Every WebSocket publish and typing command checks `room:write` and the current r

## Delivery semantics

Message create/update/delete events are emitted only after SQLite commits the corresponding state. Broadcast, presence, and typing are in-process and at-most-once; slow or failed clients are removed after the configured send timeout. Typing is never persisted, and clients must honor `expires_in` even if a stop event is missed. Reconnecting clients recover current edits and tombstones from history and current unread state over HTTP rather than relying on missed events. Clients should reconnect with backoff, consume the initial history event, and use the HTTP message cursor endpoint for older messages.
Message create/update/delete events are emitted only after the configured storage transaction commits. In supported v0.12 SQLite mode, broadcast, presence, and typing are in-process and at-most-once. In the guarded v0.13 PostgreSQL preview, application events commit to an ordered database log and are relayed across instances; connection leases and expiring typing state also live in PostgreSQL. Delivery to each WebSocket remains at-most-once, and slow or failed clients are removed after the configured send timeout. Clients must honor `expires_in` even if a typing stop event is missed. Reconnecting clients recover current edits and tombstones from history and current unread state over HTTP rather than relying on missed events. They should reconnect with backoff, consume the initial history event, and use the HTTP message cursor endpoint for older messages.

When configured, selected application webhook rows commit atomically with message/moderation state and deliver later with at-least-once semantics. Retries and manual replay keep the same `webhook-id`; each attempt gets a new signed timestamp. Delivery can be duplicated or reordered, so receivers validate the Standard Webhooks signature/timestamp and durably deduplicate IDs before side effects. See [Reliable application webhooks](WEBHOOKS.md) for the exact envelope, verification procedure, retry schedule, rotation, network policy, and recovery runbook.

Multi-worker or multi-host fan-out is not implemented in v0.12; lifecycle, webhook worker, and ban teardown are deterministic only within the supported single process. The checked-in [TypeScript client](../clients/typescript/README.md) implements the reconnect recovery sequence for browser and Node integrations. The [container profile](CONTAINER_DEPLOYMENT.md) packages exactly that one-process topology and must not be scaled to multiple replicas.
Multi-worker or multi-host fan-out is not implemented in supported v0.12 SQLite mode; lifecycle, webhook worker, and ban teardown are deterministic only within that single process. The guarded [PostgreSQL multi-instance preview](POSTGRES_PREVIEW.md) is application-wired but unreleased until its remaining process-failure and measured-load gates pass. The checked-in [TypeScript client](../clients/typescript/README.md) implements the reconnect recovery sequence for browser and Node integrations. The current [container profile](CONTAINER_DEPLOYMENT.md) packages exactly the SQLite one-process topology and must not be scaled to multiple replicas.
Loading