RCWeb Apps

Build real-time browser experiences

RCWeb App Development Guide

Build small browser apps that collaborate across phones, displays, laptops, kiosks, and other devices through one durable real-time communications layer.

The app owns the experience. RCWeb owns the connection.

An RCWeb App is a standalone HTML, CSS, and vanilla JavaScript project. Each browser joins a virtual room; the RCWeb server brokers targeted JavaScript calls between clients in that room. Application state and rendering stay in browser pages, so new app behavior does not require a new REST API, database schema, server build, or frontend toolchain.

For app-only changes, save the file and refresh the browser. The same core supports a phone controlling a shared screen, equal peers synchronising state, multiplayer games, collaborative tools, and mixed-role experiences.

HTML + CSS + JavaScript Virtual rooms WebSockets Targeted callbacks No app build step


1. The RCWeb Mental Model

RCWeb is a room-scoped message broker for browser pages. It serves each app, gives the browser a room ID and client ID, maintains a WebSocket, and forwards commands to matching clients. The Java server does not execute app JavaScript or interpret app-specific state.

1Load

A browser opens /<app-name>/ and loads the app plus /assets/core/comms.js.

2Join

rc.connect() loads app-specific setup data and joins the room selected by the URL.

3Send

The app targets a remote function call by app name, client ID, wildcard, or allow/deny list.

4Run

Matching browsers execute the call, validate its arguments, update local state, and render.

Rooms, Apps, and Clients

Concept Example Purpose
Room test-0001 Scopes a live collaboration session. Clients exchange commands only inside their room.
App quiz, quiz-c Names the browser experience and provides a convenient message target.
Client 12345678 Identifies one connected browser so replies and private state can be targeted precisely.

Keep the server generic. Define app behavior as a small remote API in browser code. A viewer can expose quiz.submitAnswer or game.jump without teaching the Java server what an answer or jump means.

2. Create an App

Required Project Structure

Give every app its own directory under src/main/apps/app/:

src/main/apps/app/my-app/
  index.html     Entry page
  style.css      App-specific presentation
  script.js      State, events, remote API, and connection setup
  appinfo.md     Markdown shown at /my-app/appinfo

Large resources that should not change during normal app editing may live in src/main/immutable/app/my-app/. Keep ordinary source and frequently edited assets with the app. Do not add remote libraries, frameworks, fonts, package managers, or build systems without explicit approval.

A Minimal index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My RCWeb App</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="description" content="A real-time RCWeb application.">
    <link rel="stylesheet" href="style.css">
    <noscript>
        <meta http-equiv="refresh" content="0; url=/assets/noscript.html">
    </noscript>
</head>
<body>
    <main id="app"></main>

    <script src="/assets/core/comms.js"></script>
    <script src="script.js"></script>
</body>
</html>

comms.js must load before the app script. Use a classic script for remote APIs; module-scoped functions are not visible to incoming calls unless they are explicitly attached to window.

A Practical script.js Starting Point

rc.onUpdateNetworkStatus = function (heading, info) {
    myApp.showStatus(heading + " - " + info);
};

rc.onUpdateError = function (error) {
    myApp.showStatus("Update failed: " + error);
};

rc.onConnected = function () {
    myApp.onConnected();
};

var myApp = (function () {
    var state = { message: "Ready" };
    var statusElement;

    var render = function () {
        document.getElementById("message").textContent = state.message;
    };

    var receiveUpdate = function (senderClient, update) {
        if (senderClient === rc.client) {
            return;
        }
        if (!update || typeof update.message !== "string") {
            console.error("Invalid update received");
            return;
        }
        state.message = update.message.substring(0, 200);
        render();
    };

    var setMessage = function (message) {
        state.message = message;
        render();
        rc.sendFunctionCall("my-app", "myApp.receiveUpdate", rc.client, {
            message: state.message
        });
    };

    var onConnected = function () {
        showStatus("Connected to room " + rc.room);
        document.getElementById("sendButton").disabled = false;
    };

    var showStatus = function (text) {
        if (statusElement) {
            statusElement.textContent = text;
        }
    };

    var init = function () {
        statusElement = document.getElementById("status");
        render();
        rc.connect();
    };

    return {
        init: init,
        onConnected: onConnected,
        receiveUpdate: receiveUpdate,
        setMessage: setMessage,
        showStatus: showStatus
    };
})();

window.addEventListener("load", myApp.init);

The object returned into the top-level var myApp is globally reachable in a classic script, so peers can call myApp.receiveUpdate. Only expose methods that are meant to be remote entry points.

3. Connection Lifecycle

Setup is asynchronous. Calling rc.connect() does not immediately make room and client values available.

1Before Connect

Define hook overrides, the global remote API, local state, rendering, and event handlers. Do not read rc.room, rc.client, or rc.commsWebSocket yet.

2Setup

rc.connect() loads the app's setup.js. Setup supplies the server version, app name, validated room ID, generated client ID, and WebSocket URL.

3Socket Open

comms.js opens the WebSocket, marks rc.connected true, increments rc.connectionCount, calls rc.onConnected(), and flushes queued sends.

4Reconnect

Closed connections retry automatically. rc.onConnected() runs after every successful reconnect, so use it to restore room-dependent UI and request current state again.

Do not attach duplicate listeners during reconnect. Bind DOM events once in init(). Keep onConnected() idempotent and reserve it for connection-aware state, presence announcements, and refresh requests.

Core Runtime Reference

API or Hook Use Important Detail
rc.connect() Load setup and connect, or reconnect an already configured client. Define overrides before calling it.
rc.disconnect() Close the current socket and cancel pending reconnect timers. Use for an intentional shutdown or controlled restart.
rc.send(js, target) Send deliberately constructed JavaScript. Returns sent, queued, or queued_replaced_oldest.
rc.sendFunctionCall(...) Serialize arguments and invoke a named global function remotely. Preferred for ordinary app messages.
rc.onConnected() Start work that needs room, client, app, or socket values. Runs after initial connection and reconnects.
rc.onUpdateNetworkStatus() Show connection and queue status in the UI. Useful for visible offline feedback.
rc.onUpdateClients(clients) React when the room's connected client list changes. The list contains client IDs, not app-owned state.
rc.onUpdateSuccess(js) Observe successful execution on the receiving browser. It is not automatically an acknowledgement to the sender.
rc.onUpdateError(error) Surface a remote call that threw on the receiving browser. Log visibly; send a deliberate acknowledgement if the sender needs one.
rc.onNoUpdate() Observe an idle keepalive. Usually left at its default.
rc.sendFileChunk(...) Supply requested data to the browser-hosted file proxy. Override only in apps that host files.

Offline Sends and the Queue

If the WebSocket is temporarily unavailable, rc.send queues messages in first-in, first-out order. The core keeps at most 200 queued sends; once full, it drops the oldest entry and reports the condition through rc.onUpdateNetworkStatus. Do not treat the queue as durable storage. After reconnect, request or send an authoritative snapshot so dropped or stale transient events cannot leave the experience inconsistent.

4. Messaging and Targeting

Prefer Remote Function Calls

rc.sendFunctionCall(
    "scoreboard",
    "scoreboard.addPoint",
    rc.client,
    "blue"
);

rc.sendFunctionCall serializes each argument with JSON.stringify and constructs a call to the named global function. Keep the function name static and validate arguments in the receiver. Objects, arrays, strings, numbers, booleans, and null values work naturally; functions and cyclic objects do not.

Use Raw JavaScript Deliberately

rc.send(
    "document.location.href='/v/?r=" + encodeURIComponent(rc.room) + "';",
    "v"
);

Raw rc.send is appropriate when the command is intentionally browser code, such as redirecting a viewer or installing generated trusted markup. Never concatenate untrusted user text into a JavaScript string. Switch to sendFunctionCall whenever the task can be represented as data passed to a known function.

Target Selectors

The target is evaluated only among clients in the sender's room. Whitespace is ignored in the selector list.

Selector Recipients Typical Use
chat Every client whose app name is exactly chat. Broadcast to peers running the same app.
12345678 One client with that ID. Private response or state snapshot.
* Every client in the room. Room-wide command; use sparingly.
spacewar* Apps whose names start with spacewar. Address a related app family.
*-c Apps whose names end in -c. Address controller apps.
chat,notepad Clients matching either app name. Explicit allow list.
!chat Every room client except the chat app. Deny-only selection.
chat,!12345678 All chat clients except one client. Broadcast without echoing to a particular peer.
!c,*-c Every client except the default and suffixed controller apps. Target display-side experiences.

Send intents, not implementation. Prefer game.jump(playerId) or poll.submitVote(optionId) over sending DOM mutations. The receiver can validate the intent, own its state, and render consistently across versions.

5. Choose a Collaboration Pattern

Asymmetric: Viewer + Controllers

A landscape display owns the shared world. One or more phones send user intents and receive private acknowledgements or state. This is the default for games, presentations, dashboards, and signage.

  • Keep authoritative state in the viewer.
  • Include rc.client with controller actions.
  • Target the viewer app, not every room client.
  • Send snapshots back to controller app names or individual client IDs.

Symmetric: Equal Peers

Every browser runs the same app and can change shared state. This fits chat, notes, drawing, voting, shared instruments, and lightweight collaborative tools.

  • Include sender and event IDs.
  • Ignore echoes and deduplicate repeated events.
  • Define a deterministic conflict rule.
  • Request current state on join and reconnect.

Authoritative Viewer Example

var scoreboard = (function () {
    var scores = { blue: 0, red: 0 };

    var addPoint = function (senderClient, team) {
        if (typeof senderClient !== "string") {
            return;
        }
        if (team !== "blue" && team !== "red") {
            return;
        }
        scores[team] += 1;
        render();
        sendSnapshot(senderClient);
    };

    var requestState = function (requesterClient) {
        if (typeof requesterClient === "string") {
            sendSnapshot(requesterClient);
        }
    };

    var sendSnapshot = function (target) {
        rc.sendFunctionCall(target, "scoreboardControl.receiveState", scores);
    };

    var render = function () {
        document.getElementById("blueScore").textContent = scores.blue;
        document.getElementById("redScore").textContent = scores.red;
    };

    return {
        addPoint: addPoint,
        requestState: requestState
    };
})();

rc.connect();

The controller sends scoreboard.addPoint(rc.client, "blue"). The viewer checks the input, changes authoritative state, renders, and acknowledges with a snapshot targeted to the sending client. Controllers do not calculate scores independently.

6. State, Joining, and Reconnecting

The server routes commands but does not retain application history. Each app must decide where truth lives and how a newly opened or reconnected client catches up.

App Shape Source of Truth Join / Reconnect Strategy
Viewer + controllers The viewer Controller sends requestState(rc.client); viewer replies directly.
Symmetric document Peer snapshot plus revision rule New peer requests state; existing peers debounce replies; receiver accepts the selected newest snapshot.
Event stream Validated event history in peers Share a bounded history and deduplicate by event ID.
Ephemeral control Current viewer state Ignore old motion events; send a fresh control state after reconnect.

A Reliable Refresh Flow

  1. On every rc.onConnected(), request current state from the authority or peers.
  2. Include the requester's client ID so the response can be private.
  3. Debounce peer responses to avoid a refresh storm when several clients join together.
  4. Include a revision, timestamp, or event ID and define exactly which snapshot wins.
  5. Render from the accepted state; do not replay stale queued pointer or control events over it.

Valid Room IDs

Use short test rooms such as test-0001 or mw01-t001. Valid room IDs contain 4 to 11 ASCII letters, digits, and hyphens; generated rooms normally look like abcd-efgh. If an invalid ?r= value is supplied, setup silently generates a replacement. Two pages opened with the same invalid value can therefore end up in different generated rooms and appear unable to communicate.

Debounce Noisy Inputs

var sendTimer;

var queueTextUpdate = function () {
    window.clearTimeout(sendTimer);
    sendTimer = window.setTimeout(function () {
        rc.sendFunctionCall("notes", "notes.receiveText", rc.client,
            document.getElementById("editor").value);
    }, 120);
};

Typing, sliders, drawing, pointer movement, orientation sensors, joysticks, and animation state should be debounced, throttled, sampled, or reduced to meaningful intents. Network frequency is part of the app design.

7. Share Large Files Through a Browser Host

Do not put large binary data inside JavaScript messages. Keep the File or Blob in the source browser and publish an active proxy URL:

var safeName = encodeURIComponent(file.name);
var fileUrl = "/x-file/" + rc.room + "/" + rc.client + "/" + fileId + "/" + safeName;

rc.sendFunctionCall("gallery", "gallery.receiveFile",
    rc.client, file.name, file.type, fileUrl);

When another browser requests the URL, RCWeb calls rc.sendFileChunk(fileId, start, url) in the hosting page. The host slices the requested range and uploads it to the supplied URL:

rc.sendFileChunk = function (fileId, start, url) {
    var file = sharedFiles[fileId];
    var chunkSize = 1000000;
    var end;
    var xhr;

    if (!file) {
        console.error("Missing shared file " + fileId);
        return;
    }

    end = Math.min(file.size, start + chunkSize);
    xhr = new XMLHttpRequest();
    xhr.open("PUT", url);
    xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
    xhr.setRequestHeader("Content-Range",
        "bytes " + start + "-" + (end - 1) + "/" + file.size);
    xhr.send(file.slice(start, end));
};

The hosting browser must remain open. RCWeb streams the bytes from that page; it does not permanently upload the complete file. Warn users before navigation, handle missing file IDs visibly, encode file names, and validate file type and size before rendering.

8. Design for the Devices in the Room

Viewer and Shared Display

Design primarily for landscape at 1920×1080 and 960×540, then make the composition work in portrait at 1080×1920 and 450×960. Use readable type, strong contrast, simple regions, and scaling that survives a distant audience.

Controller and Phone

Optimise for portrait touch use, large tap targets, concise labels, clear connection feedback, and one-handed operation where practical. Disable actions until the first connection if they require rc.client or room state.

Compatibility Contract

  • Write ES5-style JavaScript: var, functions, object literals, and string concatenation.
  • Avoid arrows, classes, modules, template literals, destructuring, spread, promises, and async/await unless explicitly approved.
  • Use conservative CSS available around 2015: media queries, positioning, transforms, transitions, and simple flexbox where suitable.
  • Avoid CSS Grid, custom properties, container queries, :has(), nesting, and other modern-only features unless the target devices are explicitly changed.
  • Use feature detection for optional browser APIs such as sensors, camera, fullscreen, and media playback.

Lean Paint for Older Displays

Some signage browsers have slow CPU painting and weak or absent GPU compositing. Gradients, backdrop-filter, filter, and large box-shadow effects can cause multi-second repaints. Include a lean-paint path that switches to flat colours and removes expensive effects. The stack-snap app is the reference pattern.

9. Security and Trust Boundaries

RCWeb deliberately executes received JavaScript in matching browser pages. That makes the protocol expressive, but it also means a room is a trusted collaboration context—not a general-purpose hostile-code sandbox.

RCWeb Contains

  • App code executes in browser pages, not the Java process.
  • Room routing separates unrelated live sessions.
  • App and client targets reduce the recipient set.
  • Each page has its own DOM, globals, and local state.
  • Browser permission and origin rules still apply.

The App Must Enforce

  • Who may join when identity or access control matters.
  • Argument type, range, length, and state validation.
  • Role and turn rules for every remote intent.
  • Safe rendering and URL/file validation.
  • Rate limits and abuse handling where required.

Remote Boundary Checklist

  • Expose the smallest possible global API; keep internal helpers private inside an object or closure.
  • Validate every remotely callable method as though its arguments came from another device—because they did.
  • Use textContent for user text. Reserve innerHTML for trusted, controlled markup.
  • Keep API keys, model credentials, passwords, and private tokens out of browser source.
  • Prefer sendFunctionCall; never concatenate untrusted values into raw JavaScript.
  • Treat room IDs as routing identifiers, not authentication secrets.
  • Make rejected actions visible in the controller UI or logs; do not silently pretend they succeeded.

10. Test the Shared Experience

Fast Local Loop

  1. Open the app at http://localhost:8080/my-app/?r=test-0001.
  2. Open its peer or controller with the same valid room, for example http://localhost:8080/my-app-c/?r=test-0001.
  3. Refresh after source edits. App-only HTML, CSS, and JavaScript changes do not require Maven or a server restart.
  4. Inspect both browser consoles. The receiving page reports remote execution errors.
  5. Watch the visible connection status and confirm the actual room ID on both pages.

Behavioral Test Matrix

Area What to Prove Common Failure
Connection Both pages show the same room and recover after a disconnect. Using room/client values before onConnected.
Targeting Only the intended app or client receives each command. Broadcasting to * or echoing to controls accidentally.
Late join A new page reconstructs current state without manual action. Only sending transient changes with no snapshot flow.
Validation Malformed, duplicate, stale, and out-of-turn calls are rejected visibly. Trusting the controller to enforce viewer-owned rules.
Performance High-frequency input stays responsive and network traffic remains bounded. Sending every pointer or sensor event.
Responsive UI Phone, landscape viewer, and portrait viewer sizes remain readable without overflow. Testing only one desktop viewport.
Weak display Lean-paint mode avoids expensive repaints. Large shadows, filters, and animated gradients.

Definition of Done

  • The page loads comms.js before the app script and connects only after hooks are defined.
  • Room-dependent startup lives in rc.onConnected() and remains safe on reconnect.
  • Remote callbacks are global, minimal, validated, and tested with invalid inputs.
  • Target selectors reach exactly the intended roles and avoid unwanted echoes.
  • The state authority and late-join refresh flow are explicit.
  • Noisy updates are rate-limited and stale queued events cannot overwrite a current snapshot.
  • Errors, rejected actions, missing files, and disconnects are visible to the user or developer.
  • The controller is comfortable on a phone and the viewer works at all four target dimensions.
  • Older displays have a lean-paint route when the design uses expensive visual effects.
  • appinfo.md explains the experience, roles, controls, and any important limitations.

Start with one room and two pages

Build the Smallest Complete Collaboration

Choose who owns state, expose a tiny remote API, connect two browsers, and make one useful action survive a reconnect. RCWeb takes care of the transport; the app can stay focused on the experience.

Explore Existing Apps AI App Development

DocumentationServer TelemetryServer StatsServer HTTP LogServer WebSocket Log