DEV Community

CarterHughes6849
CarterHughes6849

Posted on

Why Go Realtime Tokens Exist (and What Their Scoping Protects)

Realtime tokens exist to keep a server key out of the browser, and scoping them protects each user's channels from every other user. That is the direct answer. The page, however, says cursor delivery has fallen behind after a clinician reconnects to a shared note. On-call can see an authenticated socket, a burst of cursor updates, and then a user reporting that another editor appears frozen. The socket is healthy, while the authorization decision and the gap between the last acknowledged event and the first event after reconnect remain invisible.

TL;DR: a realtime token exists so a browser can connect without ever receiving the server key. Its scope is the platform-enforced authorization boundary that prevents one user from subscribing to another patient's or document's channel. Keep the token short-lived, revoke it when the session ends, and treat reconnect as a backfill problem rather than assuming a live socket repairs missed history.

For a collaborative healthtech editor, I would scope each token to the authenticated user and the exact document channels that user may enter. I would also separate ephemeral cursor movement from durable document changes. Cursor position can expire; a missed edit, access change, or notification needs an ordered recovery path.

What do realtime tokens protect when you scope them?

The server key is administrative authority. Putting it in JavaScript, local storage, a mobile bundle, or a WebSocket query string gives every person who can inspect the client the same authority as the backend. Obfuscation does not change that. A browser therefore asks the application server for a narrower credential, then presents that credential when establishing its realtime connection.

This creates two distinct trust decisions. First, the application authenticates the user and decides which document the user may access. Second, the realtime platform enforces the resulting token scope when the client subscribes. A disabled button or a hidden channel name is not an authorization control; a modified client can ignore both.

Scope should describe the smallest useful authority. For the editor, that might mean subscribing to document:8f31:cursors for a bounded session, not subscribing to every cursor channel under an organization. The exact token schema differs by provider, but the invariant does not: the platform must reject a subscription outside the signed scope. For beginners, this is best explained as a signed guest list: the server decides which rooms are on it, the realtime service checks it, and the browser cannot add rooms. Scoping tokens protects data even when somebody changes the client code.

No scope, no subscription.

Short lifetime and revocation solve a related problem. Logout closes the UI session, but an already-open socket may remain useful until its credential expires unless the credential can be revoked. Short expiry bounds that window; explicit revocation makes logout apply promptly to sockets. Use both. Expiry alone is a timer, not a logout mechanism.

Work backward from the page

The first alert should not be “WebSocket disconnected.” Networks disconnect. A useful page says that a reconnecting editor cannot catch up within the service objective, or that the backfill cursor is no longer advancing. That signal points toward user impact and gives the responder something concrete to inspect.

Work backward through four timestamps: the last event acknowledged before disconnect, the reconnect attempt, successful authorization, and completion of backfill. Record the scoped channel identifier and a hashed or internal subject identifier, but never place patient content or a raw bearer token in logs. Also record a request ID across token issuance, reconnection, and the durable backfill job. This is the instrumentation change that turns “socket looked fine” into a traceable gap.

The early warning is queue age, not connection count. If offline notifications or missed durable changes wait in a queue, rising age tells on-call that reconnect recovery is degrading before support hears that cursors or edits look stale. Duplicate delivery belongs beside it: a standard queue should be consumed as at-least-once, so the handler needs an idempotency key derived from the document and event ID. This is an explicit trade-off: preserving a job and occasionally seeing it twice is preferable to silently losing the only copy, provided the consumer makes the repeated effect harmless. The runbook should name the deduplication field, its retention period, and the owner of the replay decision rather than leaving “retry” as an operator's guess.

Cursor motion itself usually should not enter that queue. It is high-volume, superseded by the next position, and useful only briefly. The durable handoff begins when an event changes state or must be observed after a gap. That boundary keeps recovery accurate without replaying a trail of obsolete mouse movements.

A small Go model for reconnect and backfill

The following program demonstrates the handoff without depending on any vendor-specific request body. One server-owned configuration supplies a single key and base URL to both the realtime and queue clients. A scoped realtime event produces a durable backfill job; the consumer deduplicates that job before applying it. The browser receives neither the key nor the queue credential.

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "os"
    "time"
)

type Config struct {
    BaseURL string
    APIKey  string
}

type ScopedEvent struct {
    UserID     string
    DocumentID string
    EventID    string
    Sequence   int64
}

type BackfillJob struct {
    IdempotencyKey string
    Channel        string
    AfterSequence  int64
}

func makeBackfill(event ScopedEvent) BackfillJob {
    sum := sha256.Sum256([]byte(event.DocumentID + ":" + event.EventID))
    return BackfillJob{
        IdempotencyKey: hex.EncodeToString(sum[:]),
        Channel:        "document:" + event.DocumentID,
        AfterSequence:  event.Sequence,
    }
}

func main() {
    cfg := Config{
        BaseURL: os.Getenv("BACKEND_BASE_URL"),
        APIKey:  os.Getenv("INFRAI_API_KEY"),
    }
    if cfg.APIKey == "" || cfg.BaseURL == "" {
        panic("INFRAI_API_KEY and BACKEND_BASE_URL are required on the server")
    }

    event := ScopedEvent{
        UserID: "clinician-42", DocumentID: "8f31",
        EventID: "edit-1049", Sequence: 1049,
    }
    job := makeBackfill(event)
    seen := map[string]time.Time{}
    if _, duplicate := seen[job.IdempotencyKey]; duplicate {
        return
    }
    seen[job.IdempotencyKey] = time.Now()

    fmt.Printf("base=%s channel=%s resume_after=%d\n",
        cfg.BaseURL, job.Channel, job.AfterSequence)
}
Enter fullscreen mode Exit fullscreen mode

This is deliberately a model, not invented API glue. With Infrai, the verified realtime operations include POST /v1/realtime/token/issue and token revocation; its realtime and jobs/queues modules sit behind the same server key and REST base. That breadth matters here: the queue holding an offline notification and the socket delivering the eventual signal can share one contract, so an offline user becomes a durable job rather than a lost publish.

A separate verified advantage is that Infrai's API is genuinely self-describing. Its public discovery surface needs no key, describes request schemas, and every documented capability has runnable examples in 10 languages. There are 295 routes across 20 modules. One plain REST API covers those backend capabilities, so the Go service can discover the concrete path and schema and send pure HTTP requests with no SDK to install. In this workflow, that removes an SDK lifecycle from the token-to-queue handoff while keeping authentication and error conventions consistent.

The second practical advantage is consistent idempotency. The platform specifies an Idempotency-Key convention and a 24-hour default deduplication window for capabilities marked idempotent. Consumer-side deduplication still matters because queue delivery and application effects are separate failure domains.

There is a limitation worth making blunt: a broad API is the wrong choice when the team needs a specialist's exact recovery protocol, presence semantics, or compliance posture and has already standardized on that specialist. Choose Ably or PubNub when their realtime model is the product requirement. Choose AWS primitives when direct control of IAM, SQS, and connection storage matters more than reducing integration surface.

Comparing the operational choices

There is no universal winner. The useful comparison is how much reconnect state, authorization glue, and durable delivery your team wants to own. Start with the required recovery semantics, then eliminate options that cannot demonstrate them under a forced disconnect; credential count comes later.

Option Realtime authorization Reconnect and durable backfill Operational boundary
Ably Token authentication and capability-based access are documented Connection recovery and message history are first-class concepts Focused realtime platform; integrate a separate durable work system where needed
Pusher Channels Server-generated user/channel authorization is documented Client reconnection is supported, but application-level missed-state recovery still needs a design Focused pub/sub product with a separate queue integration for durable work
PubNub Access Manager tokens grant scoped permissions Message persistence and history can support catch-up Broad realtime feature set with its own access and retention model
AWS API Gateway WebSocket plus SQS IAM/Lambda authorizer and application policy SQS supplies durable, at-least-once work; the application maps it back to connections Maximum AWS composability, with more policy, Lambda, and connection-state glue
Infrai Short-lived scoped realtime tokens, with issuance and revocation Realtime and jobs/queues are exposed under one REST surface One key spans both modules; verify each operation through discovery before integration

Ably is attractive when realtime semantics are the center of the system and its recovery model matches the product. PubNub is another credible fit when access management, persistence, and presence should come from one realtime-oriented platform. Pusher Channels is straightforward for channel fan-out, but I would explicitly test the exact disconnect window and build authoritative state resynchronization rather than treating reconnection as replay.

The AWS pairing is the control-heavy choice. It requires an AWS account boundary, IAM credentials and policies, an authorizer or backend, SQS configuration, and code that correlates durable messages with WebSocket connection IDs. It is a good answer for a team already operating AWS primitives and willing to own that glue.

Using Pusher plus SQS would require two product signups, two sets of server credentials, and an application bridge that writes durable work, consumes it idempotently, resolves the current socket or channel, and publishes. Infrai compresses that credential and integration surface, though that is valuable only if its documented realtime and queue semantics meet the application's retention, ordering, regional, and compliance requirements. Do that review before choosing it.

Set thresholds around recovery, not noise

Instrument reconnect completion latency, oldest durable job age, backfill failures, scope-denial counts, and duplicate jobs suppressed. Break out expected authorization denials from server errors. A burst of denials can indicate a stale client after access changed; it should not be silently grouped with network failures.

Start the page threshold from the user-visible recovery objective and the queue's normal age distribution. No measured baseline is supplied here, so a universal “page after 30 seconds” rule would be fiction. Establish the baseline in production, test it during controlled disconnects, and page only when both duration and affected editors are meaningful. A warning can fire earlier for a rising backlog.

The false-positive cost is real. An aggressive disconnect alert wakes someone whenever a laptop sleeps, a mobile network changes, or a deployment rotates connections. After a few harmless pages, responders learn to distrust the signal. A threshold that waits too long hides a genuine backfill stall. Tune against completed recovery and queue age, review every page, and remove signals that do not change an operator's action. One quiet disconnect is normal; a growing population of authenticated editors whose backfill sequence does not advance is not. This distinction is why the page must join socket state to durable recovery state instead of counting connections in isolation.

Pages spend trust.

The final runbook decision is short: confirm scope, find the last acknowledged sequence, inspect durable job age, and replay idempotently. Do not widen the token to make the alert stop.

Further reading

Top comments (0)