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
bun install

The my-app directory must not exist yet. This lays down a small project: bext.config.toml, tsconfig.json, package.json, .gitignore, README.md, and a src/app/ route tree with a layout, a home page and an /api/hello route. The command also initializes a git repository, then prints the next steps (bun install, then bext dev); --no-post-install skips the git step and only prints the next steps. Other starters (@bext/starter-blog, -docs, -saas, -api…) exist: see the CLI Reference.

The config files

bext.config.toml

The starter ships a minimal config. Its core (excerpt below; the generated file also has [build.css] and [compile]) declares a PRISM app that watches its source dirs for live reload and renders with ISR:

[server]
app_dir = "."

[framework]
type = "prism"

[build]
watch_dirs = ["src/app", "src/components"]
live_reload = true

[rendering]
mode = "isr"
revalidate = 60

See the Config Reference for every section.

tsconfig.json

PRISM compiles JSX straight to strings, so the TypeScript config points JSX at the framework (excerpt):

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "@bext-stack/framework",
    "moduleResolution": "bundler",
    "paths": { "@/*": ["./src/*"] }
  }
}

The root layout

The root layout.tsx receives the page as children. It can own the <html> document, as below, or pass children through and let each page render its own document (the starter does the latter). The tsconfig.json above already points JSX at the framework; you can also state it per file with the 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 that returns HTML as a string (or a promise of one); no JavaScript 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 through the platform SDK.

The session comes from createSession (the @bext-stack/framework/auth entry point), a signed cookie you declare once in src/lib/session.ts: see Loaders & Actions for how to create it and sign users in.

/** @jsxImportSource @bext-stack/framework */
import { createSdk } from "@bext-stack/framework/sdk";
import { session } from "@/lib/session"; // createSession<{ name: string }>(…)

const sdk = createSdk("my-app");

export async function loader({ request }) {
  const user = session.read(request); // null when absent, tampered or expired
  const visits = Number((await sdk.kv.get("visits")) ?? 0) + 1;
  await sdk.kv.set("visits", visits, 3600); // value + TTL (seconds)
  return { user, 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") connects to the platform SDK for your app. The SDK authenticates your app automatically and scopes all data to its app id; you do not pass a token.

Calling the platform API from a loader

A loader can also call the tRPC Platform API. Queries are GET (with a superjson input envelope when the procedure takes input), and the result you want is nested at result.data.json. Your app's cookies are not sent to manage.inklura.fr, so authenticate with a Bearer token kept in your app's secrets. auth.getSession takes no input and requires the auth:read permission:

export async function loader() {
  const token = await sdk.secrets.get("INKLURA_TOKEN");
  const res = await fetch("https://manage.inklura.fr/api/trpc/auth.getSession", {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (!res.ok) return { me: null }; // for example 401 when the token is invalid or expired
  const body = await res.json();
  return { me: body.result.data.json };
}

The token has an expiry. An invalid or expired token counts as no credential: the call then runs as an anonymous visitor, which usually fails with UNAUTHORIZED (401). Renew the secret when that happens.

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 listens on loopback, on the port of [server] listen (3061 when not set); if that port is busy it picks a nearby free one and says so. The generated file also has port = 3000 under [server], but the server does not read that key: to serve on 3000, set listen = "127.0.0.1:3000" or run bext dev --port 3000. Useful flags: --port N, --listen host:port (for example 0.0.0.0:3000 to expose it on your network), --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

Pages render in an isolate, not in Node: built-ins such as node:crypto are missing or only partly stubbed, so a library that depends on them will not work reliably at render time. The dependable approach is to vendor a pure-JS ESM build as .js files under src/ and import it by relative path: see the SDK Overview.

Next steps

Page What it covers
SDK Overview The framework and the platform SDK
Loaders & Actions Data loading, mutations, session access
Deploy a PRISM Site Put the app on a domain with automatic TLS