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.

Note

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

Warning

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