Build Your First App
This walkthrough builds a minimal bext PRISM app from scratch: scaffold it, look at the two config files, add a root layout and a page whose loader reads the session and calls the platform SDK, then run it locally with hot reload. When you are ready to put it on a domain, continue to Deploy a PRISM Site.
Scaffold
Create a new app from the PRISM starter template:
bext new my-app --template @bext/starter-prism
cd my-app
This lays down a workspace package: bext.config.toml, tsconfig.json, a src/app/ route
tree, and a public/ directory for static assets. Other starters (-blog, -docs,
-saas, -api) exist — see the CLI Reference.
The config files
bext.config.toml
The starter ships a minimal config. This declares a PRISM app that watches its source dirs for live reload and renders with ISR:
[server]
app_dir = "."
static_dir = "public"
[framework]
type = "prism"
[build]
watch_dirs = ["src/app", "src/components", "src/lib"]
live_reload = true
[rendering]
mode = "isr"
revalidate = 600
See the Config Reference for every section.
tsconfig.json
PRISM compiles JSX straight to strings, so the TypeScript config points JSX at the framework:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@bext-stack/framework",
"moduleResolution": "bundler",
"paths": { "@/*": ["./src/*"] }
}
}
The root layout
The root layout.tsx owns the <html> document and receives the page as children. Every
file that uses JSX starts with the framework pragma:
/** @jsxImportSource @bext-stack/framework */
export default function RootLayout({ children }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<title>My App</title>
</head>
<body>{children}</body>
</html>
);
}
A component is just a function returning a string ((props) => string | Promise<string>);
nothing ships to the browser unless you add an island.
A page with a loader
A page.tsx renders the route. Put data loading in a loader — the page component never
sees request, so cookies and headers are read in the loader. Here the loader reads the
session and increments a counter in the KV store via the loopback SDK:
/** @jsxImportSource @bext-stack/framework */
import { createSdk } from "@bext-stack/framework";
import { readSession } from "@/lib/session";
const sdk = createSdk("my-app");
export async function loader({ request }) {
const session = readSession(request);
const visits = Number((await sdk.kv.get("visits")) ?? 0) + 1;
await sdk.kv.set("visits", visits, 3600); // value + TTL (seconds)
return { user: session?.user ?? null, visits };
}
export default function Page({ data }) {
return (
<main>
<h1>Hello{data.user ? `, ${data.user.name}` : ""}</h1>
<p>Page views: {data.visits}</p>
</main>
);
}
The loader's return value arrives on the page as props.data. See
Loaders & Actions for the full lifecycle — including action for
mutations, which runs before the loader on POST/PUT/PATCH/DELETE.
createSdk("my-app") reaches the platform SDK over the trusted loopback interface using your
app id — no admin token required. It scopes all data to that app id.
Calling the platform API from a loader
A loader can also call the tRPC Platform API. Queries are GET with a superjson
input envelope, and the result you want is nested at result.data.json. Because the loader
holds request, it can forward the caller's session cookie
(__Secure-authjs.session-token) to authenticate, then read the session with
auth.getSession:
export async function loader({ request }) {
const res = await fetch(
"https://manage.inklura.fr/api/trpc/auth.getSession",
{ headers: { cookie: request.headers.get("cookie") ?? "" } },
);
const body = await res.json();
return { me: body.result.data.json };
}
For a full standalone client — typed @trpc/client, pagination, and error handling — see
Call the API from a Script.
Run it
Start the dev server with hot reload:
bext dev
It serves on http://127.0.0.1:3000 by default. Useful flags: --port N,
--listen host:port, --open, --no-hmr. Edit a file under watch_dirs and the change is
picked up within a second or two. To see the routes it discovered:
bext routes
A note on npm packages
The PRISM bundler does not resolve bare npm specifiers into the render isolate, and the
isolate has no Node built-ins. A bare import "some-pkg" for an unvendored package fails at
render time. Vendor a pure-JS ESM build as relative .js files and import it by relative
path instead — see the SDK Overview.
Next steps
| Page | What it covers |
|---|---|
| SDK Overview | The framework and the loopback platform SDK |
| Loaders & Actions | Data loading, mutations, session access |
| Deploy a PRISM Site | Put the app on a domain with automatic TLS |