Client SDK

cloudbed/client is the browser half: Preact hooks wired to your server's queries and mutations over a WebSocket, plus built-in auth and a small router. The client entry (client/index.tsx) mounts into the runtime's #app element:

import { mount, useAuth, useMutation, useQuery } from "cloudbed/client";

function App() {
  const auth = useAuth();
  const todos = useQuery("todos") ?? [];
  const addTodo = useMutation("addTodo");

  return (
    <div>
      <p>Welcome, {auth.displayName}</p>
      <button onClick={() => addTodo("New task")}>Add</button>
      <ul>{todos.map((t) => <li key={t.id}>{t.text}</li>)}</ul>
    </div>
  );
}

mount(<App />);

Use mount(vnode) instead of calling Preact directly. It renders normally for CSR capsules and hydrates the server-rendered HTML when the server definition opts into SSR.

Data hooks

useQuery(name)

Subscribe to a server query by name. Returns undefined until the first result arrives, then the query's value — and re-renders automatically whenever a mutation changes the underlying data. Live updates are pushed by the server; there is nothing to invalidate or refetch.

For SSR capsules, useQuery is seeded from the embedded __cloudbed_state__ snapshot on the browser's first render. That avoids a blank flash; once the WebSocket connects, live query.data messages replace the snapshot exactly like normal CSR data.

useMutation(name)

Returns a stable async function that runs the named server mutation with whatever arguments you pass. On failure it rejects with a TransportError carrying { code, message } (the message of any Error your mutation threw).

const addTodo = useMutation("addTodo");
try {
  await addTodo(text);
} catch (err) {
  setError(err.message);
}

Auth

Every capsule gets "Sign in with Google" with zero registration. Tokens are scoped to your app's origin and user ids are pairwise per app, so capsules can't correlate a user across apps. Before sign-in, users are guests (guest:local, or guest:<name> via a ?guest=<name> URL parameter) — ctx.auth.userId works either way on the server.

The redirect round-trip is handled for you: the SDK completes the flow on /auth/callback and returns the user to the page they started from.

Web Push

Cloudbed serves the service worker and subscription endpoints for every capsule; you do not write or register your own service worker.

Call enablePush() from a click or tap handler. Browsers intentionally tie the permission prompt to user activation, and many will reject or suppress prompts started from page load effects.

import { usePush } from "cloudbed/client";

function NotifyButton() {
  const push = usePush();
  if (push.status === "unsupported" || push.status === "denied") return null;
  return (
    <button
      disabled={push.status === "loading"}
      onClick={() => push.status === "subscribed" ? push.disable() : push.enable()}
    >
      {push.status === "subscribed" ? "Disable notifications" : "Enable notifications"}
    </button>
  );
}

iOS Safari supports Web Push only for sites installed to the Home Screen. Cloudbed serves a web app manifest for every capsule so Add to Home Screen works out of the box; set page.icon to a square PNG for a crisp Home Screen icon (iOS ignores SVG and emoji favicons there).

Page metadata & styles

Every capsule is served inside a runtime HTML shell that loads Tailwind from the CDN, then your client bundle at /client.js. Set static shell metadata from the server definition with capsule({ page: { title, description, favicon, icon } }); see the Server SDK reference for validation rules and limits.

For app-level CSS, create client/styles.css. The CLI includes the raw file unchanged, the runtime serves it at /styles.css with ETag caching, and the shell links it after the Tailwind script. There is no CSS processing or minification, so local and deployed bytes match. /styles.css is reserved by the platform and cannot be used as an endpoint path.

Router

A minimal client-side router, included so a capsule needs no other dependencies:

import { Link, Route, Router, Routes, useParams } from "cloudbed/client";

function App() {
  return (
    <Router>
      <nav><Link to="/">Home</Link></nav>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/todo/:id" element={<Todo />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Router>
  );
}

function Todo() {
  const { id } = useParams();
  // ...
}