Skip to main content
Redis support has two separate roles in this project:
  1. Experimental event persistence: RedisEventStore<A, C> implements the async event-store contract.
  2. Realtime notification: RedisPubSubPublisher<C> publishes wake-up messages after commands commit.
Redis pub/sub is never the source of truth. Clients should use notifications to wake up, then read durable events, checkpoints, or read models. For Spin, Redis support also has two separate runtime paths:
  • Outbound Redis: the HTTP component opens spin_sdk::redis::Connection for persistence, queries, and publishing.
  • Redis Trigger: a separate subscriber component is invoked when Redis publishes to the configured channel.

Feature Flags

Enable the base async Redis API with redis, then choose the runtime client: RedisEventStore is async-only. It intentionally does not implement the sync EventStore trait because the current host APIs used by Spin and the counter example are async.

Redis Event Store Schema

The adapter stores event data with a small key layout under a configurable prefix. The default prefix is ddd_cqrs_es. Append is performed by one Lua EVAL script. The script validates the expected revision, allocates global sequence numbers, updates the stream revision, stores event hashes, and updates stream/global indexes atomically. This append atomicity is scoped to event writes. Redis currently does not implement AsyncAtomicIdempotentEventStore, so AsyncRepository::execute_idempotent_atomic is available for the SQL adapters only. Use SQLite, PostgreSQL, or MySQL when command handling requires the idempotency key reservation, event append, and completed result to commit in one backing-store transaction.

Basic Usage

Use a custom prefix when multiple apps share one Redis database:

Checkpoints

RedisCheckpointStore<C> implements AsyncCheckpointStore.
Projection writes and checkpoint writes are still separate operations. Projection handlers must be idempotent so a retry does not corrupt a read model.

Pub/Sub Notifications

RedisPubSubPublisher<C> is notification-only. Publish after event append and projection update succeeds.
If notification publishing fails after a command has committed, do not roll back the command. Log or emit telemetry, then allow clients to recover through durable replay from their last seen sequence.

Counter App Realtime

The counter example uses SSE/EventSource as the browser transport:
realtime=redis can also be used as a wake transport with another durable backend. It is supported with every counter-app backend:
Spin uses the Spin Redis client:
Spin gRPC is controlled separately by transport=<mode>. Use transport=both when a single Spin component should serve the browser UI, REST APIs, SSE realtime, and gRPC:
transport=grpc serves only the gRPC endpoints. Wasmtime currently supports the HTTP transport only and fails fast for transport=grpc or transport=both. When realtime=redis, the Spin example uses spin.redis.toml and starts a separate Redis trigger component subscribed to REDIS_CHANNEL. The trigger parses each CounterRealtimeMessage and records health markers in Redis: The trigger does not update projections, checkpoints, event-store data, or the browser SSE response. It is a smoke-testable subscriber that proves Spin Redis Trigger wiring is active. Environment variables: Runtime setup checklist: Spin outbound permissions must include the protocols and hosts used by the selected durable backend and by Redis realtime. The counter app manifests allow:
The Makefile derives the internal runtime values from backend-specific public variables: DATABASE_URL and DATABASE_AUTH_TOKEN are internal runtime env values. Set the public backend-specific variables in .env; pass the internal values yourself only when bypassing the Makefile. The SSE endpoint is:
It emits frames like:
The counter app keeps storage, projection, and realtime failures as typed application errors until the transport boundary. REST returns structured JSON errors, gRPC maps the same errors to tonic::Code, and server functions convert to ServerFnError only after logging. See Error Handling and Transport Mapping for the full contract. Internal details are written through tracing; set RUST_LOG=info,counter_app=debug when running local proof commands. To prove Redis realtime from a terminal command to an already-open browser, start the Spin app and open http://localhost:3000/:
Read the baseline view:
Run a REST command:
The JSON response count should increase by 1, the browser should update to the same count without refresh, and the event ledger should show the new sequence. Run the same proof through gRPC:
The gRPC response count should increase by 1, the browser should update to the same count without refresh, and Spin logs should show the Redis trigger observing the new sequence. To inspect SSE directly, run this in a second terminal before either command:
Do not set Connection: keep-alive manually on this endpoint. WASIp3 rejects that hop-by-hop header during response conversion. The stream stays open because the response body is streaming and the content type is text/event-stream. With REALTIME_BACKEND=redis, the counter app SSE route uses Redis as the wake transport. Each browser request registers a short-TTL Redis list queue. After commands commit and projections update, the publisher sends a notification to REDIS_CHANNEL and fans one wake message out to every live queue. The SSE handler treats that wake as notification-only and reads durable events after the client’s last_sequence before emitting one counter event on the open response. Idle clients do not reconnect every few hundred milliseconds. On Spin, the handler waits inside BRPOP for up to 25 seconds, emits one SSE comment keepalive with a 1 second EventSource retry interval, and continues waiting. On Wasmtime, the handler uses repeated RPOP calls with WASI async sleeps so the component can continue serving ordinary HTTP requests while it waits for Redis wake messages. Redis publishing remains a notification hook. On Spin, the optional Redis trigger sidecar observes the same pub/sub notifications and records health markers, but browser delivery uses the per-connection Redis list queues because the trigger cannot write into an already-open HTTP response owned by the HTTP component. The HTTP route does not perform a blocking Redis SUBSCRIBE; it uses the existing outbound Redis command path so Spin and Wasmtime share the same browser delivery model. Redis wake delivery is not an exactly-once guarantee. Duplicate or missed wake messages must be harmless because clients recover by replaying durable events or read models from the last observed sequence. For the counter app’s Redis backend, read-model updates and checkpoint updates are applied together with one Lua command per event. The generic projection runner contract remains store-agnostic and still requires idempotent projection handlers.

Current Limitations

Redis support is marked experimental until broader live contract coverage proves ordering, recovery, and operational behavior under production traffic. Known boundaries:
  • WasiRedisClient supports plain redis:// TCP URLs. It does not implement TLS, Sentinel, Cluster, or RESP3-specific behavior.
  • RedisEventStore has async append/load/global replay coverage, and RedisCheckpointStore has async checkpoint coverage, but Redis does not implement AsyncAtomicIdempotentEventStore.
  • Redis pub/sub is lossy notification, not durable delivery.
  • Counter SSE wake queues are best-effort notification. Durable events remain the source of truth, and clients recover through last_sequence replay.
  • The event store is async-only.
  • Generic projection writes and checkpoint writes are not one transaction unless an adapter or application adds a transaction-aware runner.
  • The counter app HTTP SSE route uses per-client Redis list wake queues instead of Redis SUBSCRIBE, because Spin outbound Redis exposes command execution while Redis Trigger runs as a separate component. Spin waits with BRPOP; Wasmtime polls with RPOP and WASI async sleeps. It is not a permanent multi-chunk WebSocket-style stream.