Skip to main content

stapel-realtime

CI coverage status license llms.txt

Realtime delivery substrate: the L1 library behind the Signal primitive (stapel_core.comm.signal). Ships the Channels/Redis transport for the core's signal-delivery seam, the two consumers every browser socket in the fleet is built from (EphemeralStreamConsumer for at-most-once Signal fan-out; ResumableStreamConsumer for hello/welcome/replay/live journals with seq dedup and a bounded replay window), the versioned v1 wire envelope, the canonical :<scope_type>:<scope_id>[:] stream key, a fail-closed per-stream authorize seam with the workspace-capability authorizer, revoke-to-kick, heartbeat with JWT-exp re-check, disconnect-on-overflow backpressure, the fleet close-code canon, build_websocket_application() host assembly with a port-aware origin guard, and five system checks. No models, migrations, views, urls or comm surface of its own; it is installed as a Django app only so its checks are registered.

Part of the Stapel framework — composable Django apps that deploy as a monolith or as microservices without changing module code.

Install

Not published on PyPI yet. Install from source:

pip install git+https://github.com/usestapel/stapel-realtime

At a glance

Fact Value
Version 0.1.1
Python >=3.11 (3.11, 3.12, 3.13)
Config axes 8
Usage surface 17
Extension points 6
Fleet dependencies stapel-core

Documentation

capabilities.json · llms.txt (for agents)

What this is

The delivery half of the fourth communication primitive.

Three primitives in stapel_core.comm address code. Function — "answer me now", the caller waits. Action — "this happened, the system must know": outbox, at-least-once, 0..N module subscribers. Task — "do the long work", the system waits, not the caller.

Signal is the fourth, and its addressee is a human looking at a screen:

Show this to whoever is watching right now.

There is no obligation to an observer who is not watching. When they look, they read current state over REST — the truth is in the database, and the value of a signal expires in seconds. Losing a signal is correct behaviour, and that one property is what lets this library be small: no outbox row, no retry, no history, no delivery receipt.

stapel_core.comm.signal() is the emitter — sixty lines of stdlib, free for every library in the fleet, a silent no-op with no backend configured. This package is everything on the other side of that call.

What it ships

Transport deliver(stream_key, frame) — the callable the core's STAPEL_COMM["SIGNAL_TRANSPORT"] = "channels" resolves to, registered from this package's AppConfig.ready(); plus deliver_frame() for journal fan-out and revoke() for the kick. Best-effort by contract: no layer, no subscriber, dead redis → the frame is dropped and nothing raises.
Two consumers EphemeralStreamConsumer (Signal fan-out, no seq, no history) and ResumableStreamConsumer (hello{last_seq}welcome → replay → live, deduplicated by seq, bounded replay window). Both are generalizations of stapel_chat.ChatConsumer, the one protocol the fleet had actually proven.
Wire envelope v1 {v, type, stream, payload, seq?} — the shape comm.signal() builds and this substrate forwards verbatim, published as a JSON schema (the deliberate exception to "an L1 library ships no schemas": the contract is shared by a backend consumer and a browser client written by different hands). Frame kind is structural — seq present means journal, absent means ephemeral.
Stream keys <mod>:<scope_type>:<scope_id>[:<topic>], built and validated by the core's comm.stream_key() (re-exported here, never re-implemented). The scope is in the name, so a group physically cannot cross a workspace.
Authorization A per-stream authorize() hook that is fail-closed: a consumer that does not implement it subscribes nobody. WorkspaceCapability is the canonical implementation — the same require_capability predicate HTTP uses.
Revoke → kick revoke(stream_key, user_id) sends a kick frame and closes 4410 immediately, rather than leaking until the client happens to reconnect.
Host assembly build_websocket_application() — origin guard (compared with the port) over core's G14 JWT stack over every installed module's routing manifest, discovered rather than listed.
System checks Five, each one a production bruise turned into a manage.py check verdict.
Test harness stapel_realtime.testing.open_stream() — an envelope-aware Channels client, so a module testing its consumer does not wire the fourth WebsocketCommunicator by hand.

Quick start

# myapp/consumers.py
from stapel_realtime import EphemeralStreamConsumer, WorkspaceCapability

class RecordingsConsumer(EphemeralStreamConsumer):
    module = "recordings"
    scope_type = "ws"
    stream_key_kwarg = "workspace_id"
    authorizer = WorkspaceCapability("recordings.read")
# myapp/routing.py — the manifest the host assembly discovers
from django.urls import path
from .consumers import RecordingsConsumer

websocket_urlpatterns = [
    path("ws/recordings/<uuid:workspace_id>", RecordingsConsumer.as_asgi()),
]
# myapp/services.py — the emit side. Note what is NOT imported: a module that
# only signals depends on the core, never on this library.
from stapel_core.comm import signal, stream_key

with transaction.atomic():
    recording.status = "ready"
    recording.save()
    signal(stream_key("recordings", "ws", recording.workspace_id),
           "recording.status",
           {"recording_id": str(recording.pk), "status": recording.status})
# asgi.py — the whole host
from django.core.asgi import get_asgi_application
from stapel_realtime.asgi import build_websocket_application

application = build_websocket_application(http_application=get_asgi_application())
# settings.py
INSTALLED_APPS += ["stapel_realtime"]      # so the system checks are registered

STAPEL_COMM = {"SIGNAL_TRANSPORT": "channels"}   # opt in; the default is "none"

STAPEL_REALTIME = {
    "ALLOWED_ORIGINS": ["https://app.example.com"],   # WITH the port if non-default
}
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {"hosts": ["redis://redis:6379/0"]},
    }
}

Install: pip install 'stapel-realtime[channels,redis]' on a host that serves sockets, and [testing] on top wherever a module tests its own consumer (that extra adds daphne, which channels.testing drags in — no reason to put an ASGI server on a production host). A module that only emits needs nothing from here at all: comm.signal() lives in the core, and that is the point.

The rule that keeps a fifth implementation from appearing

Before this library the fleet had three independent browser sockets (chat, video, studio-dialog) plus a machine peer protocol, each with its own JWT handling, its own close codes, and — twice, independently — its own resume protocol. The boundary is drawn by who is on the other end:

A human in a browser → stapel-realtime. One of our own processes → an application-level protocol (stapel-runner-protocol), and it owes an answer to "why not a Function or a Task".

The machine protocol stays separate on merit, not inertia: a dropped task.assign frame is unacceptable where a dropped signal is correct, it needs exactly-once apply keyed by (task_id, seq), and it is deliberately transport-agnostic so it can be tested without a network.

What it does not do

Not in v1, on purpose: a presence registry, an SSE fallback, one multiplexed socket for many streams (the envelope reserves stream so adding it later is not a breaking change), NATS as the signal transport (that is a future value of the core's axis, for the microservice topology), client→server commands over the socket (writes go through REST/Function), and delivering Actions to the browser as-is — an anti-pattern, because a five-minute-late "typing…" retried by an outbox is worse than no delivery at all.

License

MIT — see LICENSE.


This page is assembled by stapel-readme from docs/readme.md plus the contract artifacts in docs/. Edit the prose in docs/readme.md; the badges, facts and links above and below it are generated — do not hand-edit README.md.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

stapel_realtime-0.1.1.tar.gz (59.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

stapel_realtime-0.1.1-py3-none-any.whl (53.9 kB view details)

Uploaded Python 3

File details

Details for the file stapel_realtime-0.1.1.tar.gz.

File metadata

  • Download URL: stapel_realtime-0.1.1.tar.gz
  • Upload date:
  • Size: 59.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stapel_realtime-0.1.1.tar.gz
Algorithm Hash digest
SHA256 0c12d2e81cbf72375ad28cde3041021dbf577e05a180bee85735c94787031729
MD5 4daab035b11e1b2aea23bb132f4d9ff2
BLAKE2b-256 c2f147d1ae5d6b99bffd57d86287668711f1ffc2f56a297d559147a6872657ed

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_realtime-0.1.1.tar.gz:

Publisher: publish.yml on usestapel/stapel-realtime

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stapel_realtime-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: stapel_realtime-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 53.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for stapel_realtime-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2e21ee4f2920d9de23c76a84815d1f5ebe43ede17d8a273a90a99b73f64c7fff
MD5 601c1362eb198ebcd7004c6d00ca040a
BLAKE2b-256 2dd6d6920162f67301413600dd796640be843cabf85f6ef828513904c85a3678

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_realtime-0.1.1-py3-none-any.whl:

Publisher: publish.yml on usestapel/stapel-realtime

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.2

2 files

This release

0.1.1 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page