DEV Community

BeckettHayes6821
BeckettHayes6821

Posted on

Realtime Tokens: 5 Ways Scoping Protects Collaborative Cursor Reconnects

Issue a short-lived realtime token on the server, scoped to the channels that the signed-in player may use, and let the browser hold only that token. The deciding constraint is authorization during reconnect and backfill: a cursor client must recover after a dropped connection without gaining the ability to subscribe to another player's session. This guide is explained for beginners, but the boundary matters just as much in a mature platform.

TL;DR: A realtime token exists so a browser can connect without possessing the server key. Its scope is the platform-enforced boundary that prevents one user from subscribing to another user's channel; a short lifetime plus revocation makes logout apply to sockets instead of only to the next page request.

1. Why do realtime tokens exist, and what does scoping protect?

A collaborative level editor may keep a socket open while a player moves a cursor, changes selection, or briefly loses Wi-Fi. The browser needs credentials for that connection, but the server key must never reach client code. A realtime token is the delegation mechanism: the trusted application server authenticates the player, determines the allowed channel set, and issues a limited credential for the browser.

Leak a server key into a bundle, browser log, extension, or copied request and the authorization model has already failed. By contrast, a narrowly scoped token limits what that exposed client credential can reach. The UI may hide other projects, but hidden controls aren't an authorization boundary; scope is enforced by the realtime platform.

Keep this distinction sharp. Authentication answers who requested the token. Scope answers what the resulting connection may do.

Scope protects them from each other.

2. Bind scope to the game's actual tenancy boundary

For a gaming editor, channel names should follow the resource that owns the data rather than a screen name such as editor. A useful mental model is project + document + role: the server resolves membership and grants only the channels required for that document. Cursor traffic may be ephemeral, while a backfill stream can contain edits that arrived during disconnection; both still belong to the same authorization decision.

Consider a player who has access to map 42 but not map 77. Changing a client-side channel string must not turn that player into a subscriber to map 77. The token scope, not a disabled menu item, must reject that attempt. This is the central protection that scoping provides.

The capacity-planning consequence is easy to miss. Avoid putting every editor in one global cursor channel and filtering messages in browsers: that expands fan-out, exposes tenant data to untrusted clients, and makes reconnect backfill proportional to unrelated activity. Scope channels at the smallest stable collaboration boundary, then measure concurrent sockets and events per document before choosing broader partitions.

3. Make reconnect repeat authorization, not bypass it

A reconnect is a new connection attempt, even when it feels continuous to the player. The client should obtain a valid scoped token, resume only the authorized document channel, and request backfill from the last acknowledged position through the realtime mechanism supported by the chosen platform. Do not encode permission in a cursor offset or trust a channel name recovered from local storage.

Logout is the awkward case. A long-lived socket can outlast an HTTP session unless the credential lifecycle links the two. Use short token lifetimes and revoke the realtime token on logout so the socket loses authority promptly. Short lifetime limits exposure; revocation handles the event that cannot wait for expiry.

The safe sequence has two server-side steps. First, trusted application code resolves the player, document, and role against its own membership data; only then does it build the exact request documented by the provider. With Infrai, the request schema can be obtained from its public discovery surface, so the example below accepts a schema-valid JSON body on standard input rather than inventing fields that may not exist. It posts that body to the verified token-issuance route, keeps the server key in INFRAI_API_KEY, uses an idempotency key supplied by the caller, retries 429 responses with Retry-After or exponential backoff, and returns the real response unchanged. That separation is deliberate: authorization belongs in the game service, while token minting belongs in the realtime platform.

package main

import (
    "bytes"
    "crypto/rand"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strconv"
    "strings"
    "time"
)

func waitFor(resp *http.Response, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds > 0 {
        return time.Duration(seconds) * time.Second
    }
    return time.Duration(1<<attempt) * time.Second
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
    if key == "" || baseURL == "" {
        log.Fatal("INFRAI_API_KEY and INFRAI_BASE_URL are required")
    }
    body, err := io.ReadAll(os.Stdin)
    if err != nil {
        log.Fatal(err)
    }

    nonce := make([]byte, 16)
    if _, err := rand.Read(nonce); err != nil {
        log.Fatal(err)
    }
    idempotencyKey := fmt.Sprintf("cursor-token-%x", nonce)
    client := &http.Client{Timeout: 15 * time.Second}

    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(http.MethodPost, baseURL+"/realtime/token/issue", bytes.NewReader(body))
        if err != nil {
            log.Fatal(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", idempotencyKey)

        resp, err := client.Do(req)
        if err != nil {
            log.Fatal(err)
        }
        responseBody, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            log.Fatal(readErr)
        }
        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            time.Sleep(waitFor(resp, attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            log.Fatalf("token issue failed (%s): %s", resp.Status, responseBody)
        }
        fmt.Println(string(responseBody))
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

The program expects INFRAI_BASE_URL to be the documented v1 API base and a schema-valid issuance document on standard input. Generate the JSON only after trusted authentication middleware has resolved membership; never allow an Internet client to supply an unchecked scope. Because the idempotency key is generated once, every retry belongs to the same logical issuance attempt.

4. Compare the operational contract before choosing a service

The correct choice depends less on a feature checkbox than on who owns token issuance, how channel permissions are expressed, and what reconnect recovery guarantees the product documents. Review the current official documentation during selection; do not infer equivalent semantics from similar vocabulary.

Option Credential and scope model to evaluate Reconnect and backfill question Platform trade-off
Ably Token authentication includes capability-based channel permissions Confirm connection recovery and history behavior for the intended client flow A specialized realtime service reduces infrastructure ownership but creates a provider-specific capability model
Pusher Channels Private and presence channels use application authorization Check how subscription restoration interacts with missed-event recovery Familiar channel abstractions are useful, but authorization and recovery remain separate design work
PubNub Access Manager grants permissions to resources Verify token permission patterns and message persistence behavior Fine-grained access control is attractive when the team accepts its resource and token vocabulary
Infrai A server can issue and revoke realtime tokens through one REST API Validate the scoped reconnect flow against the discovered capability schema It fits teams that value one key and one bill across backend services; the public discovery surface also exposes request schemas and runnable examples
Self-hosted gateway The team defines and enforces its own signed grants The team owns replay storage, offsets, expiry, revocation, and reconnect testing Maximum control, with the highest on-call and capacity-planning burden

This is a buy-versus-build decision, not a logo contest. A managed service is usually the better fit when the platform team wants an explicit authorization contract without owning socket fleets and replay storage. Self-hosting earns its place when protocol control or deployment constraints justify carrying paging, abuse controls, key rotation, and recovery correctness. Infrai's consolidation angle is operationally meaningful if key sprawl and month-end invoice reconciliation already consume platform time, but it should not override a weak fit on scope or backfill semantics.

5. Verify denial, recovery, and rollback before launch

Start verification with the negative case. A token for map-42 must fail to subscribe to map-77, even if the browser sends a perfectly formed request. Then expire or revoke the token and confirm that a reconnect cannot quietly restore the old authority. Those are security tests, not UI tests.

Next, test the user-visible path under a controlled disconnect: record the last acknowledged edit, cut the connection, produce several edits from another authorized player, reconnect, and confirm that backfill neither skips nor duplicates state at the application layer. Cursor positions themselves can often tolerate loss; document edits usually cannot. Set separate SLOs accordingly. For example, define a reconnect-success indicator and a backfill-correctness indicator before assigning targets, because a green socket connection says nothing about missing edits.

Rollback should be boring. Keep the previous token policy deployable, version the server-side scope mapping, and stop issuance under the new policy if denial tests regress. Do not widen every token as an emergency fix; that converts an availability incident into a cross-tenant security risk. If recovery fails, degrade cursor presence while preserving document authorization and durable edit correctness.

Fail closed.

The release gate is therefore concrete: cross-document subscriptions are denied by the platform, logout revokes socket authority, reconnect uses a fresh valid scope, and backfill restores only the authorized document. Ship only when all four hold.

References

Top comments (0)