How it works
Recached clients keep local cache data in memory and synchronize with the server over WebSocket. The same Rust engine executes cache operations in the server, browser, native mobile SDKs, and embedded Rust client; each adapter owns its transport and persistence.
Shared engine and platform adapters
The workspace contains six crates:
| Crate / directory | Role |
|---|---|
core-engine | RESP parsing, typed commands, a sharded DashMap store, TTLs, collections, JSON, and eviction. No networking or file persistence. Uses the native clock or JavaScript Date.now() for expiry. |
server-native (package recached) | Tokio RESP TCP and WebSocket servers, authentication, scope checks, change delivery, snapshots/AOF, and replication. |
sync-client | Platform-neutral session replay, ordered reply matching, outbox, DEDUP envelopes, backoff, and applying server state to local memory. |
wasm-edge | Browser WebSocket transport, WebAssembly bindings, optional IndexedDB persistence, and opt-in BroadcastChannel sharing. |
recached-embed | Async Rust service adapter with local reads, awaited watch hydration, and an in-memory outbox. |
recached-mobile | UniFFI surface for Kotlin and Swift, local reads, transactional SQLite persistence, and changed-key reporting. No sockets or async runtime. |
The platform wrappers live in recached-kotlin and recached-swift. Kotlin owns an OkHttp WebSocket and routes changes to Flow observers. Swift owns a URLSessionWebSocketTask and routes changes to AsyncStream and SwiftUI models.
DashMap uses shard locks; it is not lock-free. Server workers execute commands concurrently, but a single hot key still contends on its shard. Single-key commands are atomic; multi-key commands and transactions do not isolate concurrent readers from intermediate results. See the concurrency model.
Connect and hydrate
Clients connect to the WebSocket port, 6380 by default. Backend RESP clients use port 6379. With server TLS configured, use wss:// for sync and rediss:// for RESP.
A socket opening does not fetch the keyspace. Register a live query with browser liveQuery(pattern), mobile watch(pattern), or Rust watch(pattern).await?. The server replies with a complete qstate snapshot for the pattern and delivers later changes. A pattern exceeding the initial snapshot limit returns an error instead of partial state.
On each reconnect, sync-client sends the session in this order:
AUTH, when a password is configuredCLIENT DELTA ONandCLIENT EXPIRY ONSYNC TOKENor open-mode sync patterns, when configured- Queued writes, oldest first
QSUBfor each active live query
Replaying writes before snapshots prevents a reconnect snapshot from temporarily erasing accepted offline edits. Applying the new snapshot also removes matching local keys absent from the server, including deletions missed while offline. Re-register mobile watches after reopening the cache; they are not stored in SQLite.
Authorization is separate from watches. With no sync secret or scopes, the default server fans mutations out to every WebSocket client. Scoped connections receive only permitted keys. Use backend-minted sync tokens for per-user data.
Read and write locally
Client reads consult local core-engine memory without waiting for the network. Until hydration completes, a browser or mobile read may return a saved value or a miss. Embedded Rust reads outside hydrated watches return NotHydrated.
Browser and mobile writes apply locally, then enter an outbox for sending or reconnect replay — or, when the outbox is full, fail before changing anything. A successful local write does not mean the server accepted it. Embedded Rust writes await the server reply; their queue is held in memory. The server can refuse a write because of authorization, type, or resource limits; see write durability and replay.
The server orders command replies on each connection. Pushes can interleave with replies, so sync-client distinguishes mutations, pub/sub messages, keychange, keydelta, and qstate. It uses a FIFO to retire the write acknowledged by each reply.
Server changes reach clients in two forms:
- Mutation pushes carry replayable commands for keys not already covered by that connection's watches or live queries. The server excludes the originating connection from this fan-out.
- Watched keys receive authoritative
keychangevalues or negotiatedkeydeltaoperations. These can reach the writing client too, to reconcile its local value with the server.
Changed-key notifications let observers re-read their key or pattern. Mobile observers suppress equal values and coalesce updates for slow consumers; they do not deliver a durable history of every mutation.
Persist browser and mobile data
Each adapter has a different storage contract:
| Adapter | What is saved | When it is saved |
|---|---|---|
| Browser | Local write commands and, by default, the state of keys the server changed, in an IndexedDB WAL; pending writes | Asynchronously: a write's WAL record and outbox row commit in one transaction after the call returns; flush() resolves once committed, and failures reach onPersistenceError |
| Native mobile | Materialized per-key snapshots, pending writes, client identity and epoch in SQLite | A successful local write commits data and its outbox row before returning, and a failed one is rolled back; received frames commit changed keys and retired rows together |
| Embedded Rust | No on-disk client persistence | Store and outbox live only in the process |
Browser startup replays the WAL and restores queued writes, sending them at once if the socket is already open. Server-applied changes are written to the WAL a fraction of a second after they arrive (turn this off with persistSyncedState: false), so an offline reload shows the last synchronized state; cross-tab messages are persisted by the tab that made the write. Compaction runs during hydration when the WAL exceeds 1,000 rows, replacing the records it read atomically with snapshot commands while leaving the outbox intact. Each page load is its own sync identity, and tabs key their rows apart, so two tabs on one IndexedDB cannot overwrite or suppress each other's writes. See browser persistence.
Mobile startup restores the saved state before opening its socket, so saved server-pushed values remain readable offline after an app restart. SQLite runs in WAL mode with synchronous = NORMAL; an app kill preserves committed writes, but OS crashes or power loss can lose recent commits. See native client limits.
Share browser writes across tabs
Setting broadcastChannel shares local write commands with tabs on the same origin and channel. Receiving tabs apply them to memory and notify listeners, but do not forward them to a server or persist them to their own WAL. Server pushes are not rebroadcast.
BroadcastChannel does not elect a connection leader or proxy server traffic. Each tab that needs server updates must connect independently. It has no role in native mobile or cross-device sync.
Where your database fits
A database is optional and sits outside Recached. When MySQL or PostgreSQL owns your records, your backend commits each change there first, then publishes the resulting cache value to the server over RESP, typically from a transactional outbox worker. Recached delivers that value to clients like any other write.
Nothing in Recached reads from or writes to SQL: there is no connector, change-data-capture pipeline, or write-through hook. Client cache writes reach the Recached server only. See database integration for the publishing pattern and for handling offline business edits.
Read the protocol contract
TCP and WebSocket carry RESP (Redis Serialization Protocol). WebSocket commands and replies use text frames for valid UTF-8 and binary frames for other bytes. Values are binary-safe; keys, fields, members, patterns, and channels must be UTF-8 text.
The wire protocol reference defines frame shapes, reply ordering, negotiated expiry/deltas/push framing, the session order behind the synchronization status, and duplicate suppression. DEDUP covers replay while the server retains the corresponding client mark, including across a server crash with an AOF; it does not guarantee exactly-once delivery in every case — the protocol page lists the exceptions.