stapel-realtime
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.2 |
| 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file stapel_realtime-0.1.2.tar.gz.
File metadata
- Download URL: stapel_realtime-0.1.2.tar.gz
- Upload date:
- Size: 60.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
caa1426a1141d19a7ecf27a008db00b6ddfc9cff3f37b22a3acb62bf198d8afc
|
|
| MD5 |
59f5af3556b385ad4a13e97f5f18750d
|
|
| BLAKE2b-256 |
6d759d9cb17387f6e53b26d58cc46c51fa6e331db7dfb16b49784bdaec41bc5a
|
Provenance
The following attestation bundles were made for stapel_realtime-0.1.2.tar.gz:
Publisher:
publish.yml on usestapel/stapel-realtime
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_realtime-0.1.2.tar.gz -
Subject digest:
caa1426a1141d19a7ecf27a008db00b6ddfc9cff3f37b22a3acb62bf198d8afc - Sigstore transparency entry: 2581052434
- Sigstore integration time:
-
Permalink:
usestapel/stapel-realtime@36db48ee17c326417c232d949aff8b3e00df6615 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@36db48ee17c326417c232d949aff8b3e00df6615 -
Trigger Event:
push
-
Statement type:
File details
Details for the file stapel_realtime-0.1.2-py3-none-any.whl.
File metadata
- Download URL: stapel_realtime-0.1.2-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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7763c3b267c10db98af2c62f49adebeb744e5b6e39304b4f0dea1ceb27cba275
|
|
| MD5 |
32b3934c471ecb043b2271f857758cfe
|
|
| BLAKE2b-256 |
00f1eed5cf1f26187b650133aeba8548abb746120369aba565218c6df78c71e9
|
Provenance
The following attestation bundles were made for stapel_realtime-0.1.2-py3-none-any.whl:
Publisher:
publish.yml on usestapel/stapel-realtime
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
stapel_realtime-0.1.2-py3-none-any.whl -
Subject digest:
7763c3b267c10db98af2c62f49adebeb744e5b6e39304b4f0dea1ceb27cba275 - Sigstore transparency entry: 2581052450
- Sigstore integration time:
-
Permalink:
usestapel/stapel-realtime@36db48ee17c326417c232d949aff8b3e00df6615 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/usestapel
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@36db48ee17c326417c232d949aff8b3e00df6615 -
Trigger Event:
push
-
Statement type: