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.4
Python >=3.11 (3.11, 3.12, 3.13)
Config axes 8
Usage surface 18
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.4.tar.gz (63.4 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.4-py3-none-any.whl (56.1 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: stapel_realtime-0.1.4.tar.gz
  • Upload date:
  • Size: 63.4 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.4.tar.gz
Algorithm Hash digest
SHA256 4a8751697ecb2ee599c9621169ce9e02da1cd8db6f5cf550032b80462fdf5ea6
MD5 cd3d8145d5ad0a5f0fade1fcb9d61e89
BLAKE2b-256 0cb8fa200a28f2db4b4245b984a770340e58faf3930ee17836b95fde088e3f99

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_realtime-0.1.4.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.4-py3-none-any.whl.

File metadata

File hashes

Hashes for stapel_realtime-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 d09eca8faf901b0040ea3a32b0cad212fe466dfe92acd554810d96f3f4370e50
MD5 de22e69ecbf304d546835437c0b99d2d
BLAKE2b-256 a91bb9406234b32b68b43405786f20718bed07b7e8482de88cf622d6897aec94

See more details on using hashes here.

Provenance

The following attestation bundles were made for stapel_realtime-0.1.4-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.2.1

2 files

0.2.0

2 files

This release

0.1.4 This release

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page