Pusher and Soketi
The managed WebSocket preset, its authorization model, and the honest limits of what terminating a connection can revoke.
Pick this when you want provider-managed WebSockets, provider-native presence
and shared multicast for channel events. pusherRealtime() fills both realtime
seams at once, so live(), { realtime: true } and client.channels.* stay
exactly as they were.
Wiring it
import { pusherRealtime } from "questpie/adapters/pusher";
import { runtimeConfig } from "questpie/app";
export default runtimeConfig({
db: { url: env.DATABASE_URL },
realtime: pusherRealtime({
appId: env.PUSHER_APP_ID,
key: env.PUSHER_KEY,
secret: env.PUSHER_SECRET,
cluster: env.PUSHER_CLUSTER,
}),
});| Option | Notes |
|---|---|
appId, key, secret | Required provider credentials |
cluster | Defaults to mt1 when no host is given |
host, port, useTLS | Self-hosted Soketi. useTLS defaults to true |
wsHost, wsPort, wssPort | Browser-side socket endpoint, falls back to host and port |
brokerChannel | Defaults to a hash of appId and key |
authEndpoint | Where the browser asks for channel auth |
clientEvents | Direct provider client events, off unless configured |
questpie/adapters/pusher is the only entry that loads the optional pusher
and pusher-js peers, so an app that never imports it never pays for them.
What travels over the provider
Live-query snapshots stay private to one edge session, because each snapshot
has already passed that principal's row access, field access and afterRead
hooks before it is written. The shared broker channel carries only wakes.
Framework channel events may use provider multicast, but only after
subscribe and publish authorization and Zod validation have both run.
Authorization
QUESTPIE posts the current socket_id, plus the channel name when it is
authorizing a channel, to the authenticated realtime auth route, which answers
with Cache-Control: no-store. The signed user id is an
HMAC of a stable principal identity keyed by your secret, so the provider
never learns your user ids. After a reconnect pusher-js asks for fresh user
authentication, and channel authorization waits for that sign-in before it
returns anything, rejecting a released owner or a stale socket.
The transport declares channelGrantMode: "stateless-signed-grant", meaning
generateAuth is side-effect-free local encoding. QUESTPIE evaluates policy and
signs outside any database lock, then runs an optimistic generation check before
handing the blob over. A concurrent revocation discards it before it reaches the
browser. A shared transport that does not declare this capability is rejected
before policy runs at all.
Revocation, honestly
channels.revokeAuthority(channel, { subject }) persists the authorization
generation in the caller's database transaction. Pusher declares
authorityRevocationScope: "principal-connections", which is the honest claim:
it can terminate every current connection for one user, but it cannot terminate
one logical channel subscription. After termination, fresh per-channel
authorization readmits the still-valid bindings and denies the revoked one. The
subject must be a user.
| Caller | Failure behavior |
|---|---|
| Inside a managed transaction | Termination and acknowledgement run inline. A failure throws and rolls back |
| Standalone command | The durable cut stands, provider dispatch stays fail-closed, an idempotent retry finishes it |
Revocation is not frame-atomic
A frame Pusher already accepted can still arrive during termination, and a conservative disconnect can outlive a later database rollback. A custom shared transport must declare its own revocation scope. QUESTPIE never assumes an unknown provider revokes exactly.
Direct client events
Off by default, and not equivalent to client.channels.*.publish(). Turning
them on takes three deliberate keys.
clientEvents: {
enabled: true,
acknowledgeProviderWideRisk: true,
allowedChannels: ["presence-chat-room-one"],
}The second key is required because Pusher enables client events for the whole provider app, not per channel. The allowlist constrains the QUESTPIE SDK only. A raw provider client can bypass it, and anything it sends skips framework schemas, publish authorization, rate limits, ordered replay and delivery guarantees.
Related
Tuning
Every key on RealtimeConfig, the admission limits that reject a topic before it costs anything, and the outbox table the whole runtime reads from.
Tag invalidation
Group KV keys under a tag and drop the whole group in one call. Every built-in adapter implements it, but the methods sit on the adapter rather than on ctx.kv, so reaching them takes one deliberate step.