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.
useAuth()— current auth state:userId,displayName,provider("guest"|"google"),isGuest,isAuthenticated,isLoading, plusemail/emailVerified/picturewhen signed in.<SignInWithGoogle />— a ready-made button; acceptsclassNameand customchildren, disables itself while auth is loading, and renders nothing if sign-in isn't configured for the deploy.signInWithGoogle()— start the sign-in flow imperatively (redirects to the auth broker and back).signOut()— drop the identity and reconnect as a guest.isSignInConfigured()— whether the deploy has sign-in available.
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.
usePush()— hook returning{ status, enable, disable }, wherestatusis"loading","unsupported","denied","unsubscribed", or"subscribed".enablePush()— register the built-in service worker, ask the browser for permission, subscribe with the capsule VAPID key, and save the subscription.disablePush()— unsubscribe the browser and remove the saved subscription.getPushStatus()— read the current browser/subscription state outside React.
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();
// ...
}
<Router>wraps the app;<Routes>renders the first matching non-wildcard<Route>, falling back topath="*"when nothing else matches.<Link to>navigates without a reload (and correctly defers to modifier-clicks and middle-clicks).useParams()— captured:paramvalues;useLocation()— current{ pathname, search, hash }.useNavigate()in components — or the module-levelnavigate(to, { replace? })outside them.