Web apps
A Reboot web app is an ordinary React single-page app. What Reboot adds is that it talks to your backend through generated, typed hooks that are reactive — React components re-render when backend state changes, without polling — and that its users sign in through your backend's own OAuth server.
This page covers how a web frontend is put together. For the full hooks reference, see Call your API from React.
Install and generate
Add the React client to your frontend's package.json:
npm install -S @reboot-dev/reboot-react
Tell rbt generate where to emit the typed client, in your
.rbtrc:
generate --react=frontend/api
Generating into frontend/api/ — a sibling of the app itself — means
every frontend of your application (web, mobile, MCP UIs) shares one
client. Keep your bundler's root at frontend/ so that directory is
inside it; if your app root is frontend/web/ instead, Vite needs
server.fs.allow: [".."] to read a sibling.
Point the client at your backend
Wrap your app in a RebootClientProvider with the backend's URL:
// frontend/web/src/main.tsx
import { RebootClientProvider } from "@reboot-dev/reboot-react";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<RebootClientProvider url={import.meta.env.VITE_REBOOT_URL}>
<Root />
</RebootClientProvider>
</StrictMode>
);
Read the URL from the environment rather than hard-coding it. A web app is served from its own origin — a CDN or static host — almost never the same origin as the backend, so a real deployment must set it:
# frontend/web/.env.development
VITE_REBOOT_URL="http://localhost:9991"
# frontend/web/.env.production
VITE_REBOOT_URL="https://your-backend-host.example.com"
Signing in
Reboot's OAuth server does the work; your app needs three hooks.
import { useUser } from "@api/todos/v1/todos_rbt_react";
import { useSignIn, useSignOut } from "@reboot-dev/reboot-react";
export const Root = () => {
const { user, isLoading } = useUser();
const signIn = useSignIn();
const signOut = useSignOut();
// The provider renders immediately and resolves the session in the
// background, so `isLoading` holds until it lands.
if (isLoading) {
return <p>Checking session…</p>;
}
if (user === undefined) {
return <button onClick={() => signIn()}>Sign in</button>;
}
return <App user={user} onSignOut={() => void signOut()} />;
};
useSignIn()returns a function that starts the sign-in flow. The browser goes to your identity provider and comes back signed in, to where the user was. Pass areturnToto send them somewhere else.useSignOut()drops this device's session. The identity provider's own session is left alone.useUser()is generated from yourUsertype, so it is typed to your API. Called with no ID it resolves the signed-in user's ownUser— the instance Reboot auto-constructed at sign-in.userisundefinedwhen nobody is signed in.
The user handle exposes your User methods: reads as nested hooks,
mutations as plain calls.
const { response } = user.useListTodoLists();
await user.createTodoList({ title: "Groceries" });
isLoading firstWhile isLoading is true, user is still undefined. Reaching the
undefined check after loading has finished is what genuinely means
"nobody is signed in".
Other state types
Generated hooks exist for every state type, not just User:
const todoList = useTodoList({ id: todoListId });
const { response } = todoList.useTodos();
await todoList.add({ todo: "Milk" });
Where does todoListId come from? From the person's User, the one
instance you can reach without an ID: it records what they own, and a
component reads those IDs off it and passes each one down.
const TodoLists = ({ user }: { user: UseUserApi }) => {
const { response } = user.useListTodoLists();
return (response?.todoLists ?? []).map((todoList) => (
<TodoListView key={todoList.todoListId} id={todoList.todoListId} />
));
};
const TodoListView = ({ id }: { id: string }) => {
const todoList = useTodoList({ id });
const { response } = todoList.useTodos();
return (
<ul>
{(response?.todos ?? []).map((todo) => (
<li key={todo}>{todo}</li>
))}
</ul>
);
};
Backend configuration
Two things on the backend matter to a web frontend.
1. allowed_origins. Your SPA is on a different origin than the
backend, so it must be allow-listed for credentialed requests:
oauth=OAuth(
provider=...,
allowed_origins=["https://app.example.com"],
)
In development, http://localhost and http://127.0.0.1 on any port
are allowed automatically. In production, omitting allowed_origins
is a startup error. See Configure OAuth.
2. The callback URL. Register
<your-backend-url>/__/oauth/callback with your identity provider —
the backend's URL, not the SPA's.
Local development
Run two processes: the backend, and the Vite dev server.
rbt dev run
cd frontend && npm run dev
rbt dev run generates the React client before it starts, so the dev
server always has an up-to-date frontend/api/.
If you want Reboot to serve the frontend too — so both are on one
origin, and so UI methods work — point it at your frontend root in
.rbtrc:
# The frontend root, shared by the `:hmr` and `:dist` configs below.
dev run --frontend-root-path=frontend
# HMR: proxy `/__/frontend/**` to the Vite dev server.
dev run:hmr --frontend-host=http://localhost:4444
dev run --default-config=hmr
# Dist: serve pre-built assets instead. `rbt dev run --config=dist`.
dev run:dist --frontend-dist-path=frontend/dist
See Develop locally for what each flag does.
Going to production
- Build the SPA (
npm run build) and publish it to any static host. - Set
VITE_REBOOT_URLto the backend's public origin at build time. - Deploy the backend — Reboot Cloud or your own infrastructure.
- Add the SPA's origin to
allowed_origins, and the backend's public URL plus/__/oauth/callbackto your identity provider.
Browsers only use HTTP/2 over TLS. Over plain HTTP, every reactive
reader holds its own WebSocket, and browsers allow only around 200
of those per page (255 in Chrome), so an app with many concurrent
readers needs HTTPS.
See the rbt CLI
for local certificates.
Next
- Call your API from React — reactive reads, optimistic updates, offline caching, Next.js server components.
- Users and sign-in — what
useUser()is resolving. - React Native apps — the same code on a phone (alpha).