# Pub/Sub service (`devplacepy/services/pubsub/`, `devplacepy/routers/pubsub.py`)

This file documents the database-free publish/subscribe bus. Claude Code auto-loads it when a file under `devplacepy/services/pubsub/` is read or edited.

## Overview

`pubsub.py` (router, mounted at `/pubsub`) is a **database-free** publish/subscribe bus. `WS /pubsub/ws` (subscribe/unsubscribe/publish frames, `foo.*` wildcards), `POST /pubsub/publish` + `GET /pubsub/topics` (admin/internal). Lock-owner-gated WS (`4013` retry) so subscribers converge on one worker; topic authz in `services/pubsub/policy.py` (`user.{uid}.*` private, `public.*` shared, admin/internal anywhere, guests opt-in). In-process `services.pubsub.publish(topic, data)` for backends; frontend `app.pubsub`.

A general-purpose, **database-free** publish/subscribe bus mounted at `/pubsub`, for live broadcast between frontend tabs, background services, and Devii without polling.

## Hub (`services/pubsub/hub.py`)

`PubSubHub` keyed by topic pattern -> live websockets (no persisted buffer; pub/sub is fire-and-forget). `subscribe/unsubscribe/drop_socket`, `async publish(topic, frame) -> delivered` (drops dead sockets), `topics()`. Wildcards: a subscription to `foo.*` matches `foo` and `foo.bar` (`topic_matches`). Module singleton `pubsub`; `services.pubsub.publish(topic, data)` is the in-process helper for backends.

## Single-worker model

The `WS /pubsub/ws` is gated to the service lock-owner worker with the `4013` fast-retry close code (like `/devii/ws`), so every subscriber transparently converges on one worker and the in-process fan-out reaches everyone with no broker and no DB. Deliberate, documented trade-off: ephemeral and best-effort (a crash drops in-flight messages), matching the `background` queue's durability stance.

## Topic authz (`services/pubsub/policy.py`)

`resolve_actor` reuses the dbapi caller (admin/internal) then falls back to user/guest. `can_subscribe`/`can_publish`: admin/internal anywhere; users publish only to their own `user.{uid}.*` namespace and subscribe to that plus `public.*`; publishing to `public.*` is admin/internal only (prevents cross-user spam); guests get read-only `public.*` when `pubsub_allow_guests` is on. Topic names match `^[A-Za-z0-9_.*-]{1,128}$`; payloads capped at 64 KB.

## HTTP + introspection

`POST /pubsub/publish` (admin/internal; returns `409` on a non-owner worker, retry), `GET /pubsub/topics` (admin/internal). Admin/internal publishes record a `pubsub.publish` audit event.

## Frontend

`app.pubsub` (`static/js/PubSubClient.js`) is a lazy reconnecting client: `app.pubsub.subscribe(topic, cb)` / `app.pubsub.publish(topic, data)`, `4013`-aware fast retry, re-subscribes on reconnect, client-side wildcard dispatch.

## Service + nginx

`PubSubService` (`default_enabled=True`, no-op run loop) surfaces the `pubsub_allow_guests` toggle on `/admin/services` and gates the WS via `is_enabled()`. `/pubsub/ws` has its own nginx upgrade location.
