Overview
Inklura apps, from the console to the tenant sites, are bext PRISM apps. PRISM is two things:
a server-rendering framework (@bext-stack/framework) and a platform SDK each app uses to
store and process its data.
- Framework: a zero-runtime JSX-to-string engine. There is no React, no virtual DOM and no client runtime by default. A component is a function that returns a string.
- SDK: KV, queue, tasks, cache, secrets, a scheduler and relational entities. The SDK authenticates your app automatically and scopes all data to it.
The component model
A PRISM component is a plain function:
import type { Component } from "@bext-stack/framework";
// type Component<P = {}> = (props: P) => Renderable;
Renderable covers strings (and numbers, booleans, null and undefined), arrays of renderables,
a promise of a renderable for async components, and an async iterable for streaming. JSX compiles
straight to string building: there is no reconciliation, and nothing ships to the browser unless
you add an island.
Every source file that uses JSX starts with a pragma comment:
/** @jsxImportSource @bext-stack/framework */
Interactive island files (client-side signals) start with the "use signals" directive,
followed by a different pragma:
"use signals";
/** @jsxImportSource @bext-stack/framework/signals */
Site layout
A PRISM site is a package:
my-app/
├─ bext.config.toml # site + rendering config
├─ tsconfig.json
├─ package.json
├─ src/
│ ├─ app/ # file-based route tree (page.tsx, layout.tsx, route.ts, …)
│ ├─ lib/
│ └─ components/
├─ public/ # static assets served as-is
│ └─ islands/*.js # client-side islands
└─ .bext/ # compiler cache, gitignored
package.json declares the framework as a dependency:
{
"dependencies": {
"@bext-stack/framework": "workspace:*"
}
}
tsconfig.json wires JSX to the framework:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@bext-stack/framework",
"moduleResolution": "bundler",
"paths": { "@/*": ["./src/*"] }
}
}
.bext/ is the compiler cache. It is gitignored: do not commit it. See
CLI Reference for the dev/build workflow and
Config Reference for bext.config.toml.
See Routing & Rendering for what goes in src/app/.
The createSdk client
The framework ships the SDK client in its sdk entry point:
import { createSdk } from "@bext-stack/framework/sdk";
const sdk = createSdk("<app-id>", { timeoutMs: 30_000 }); // options are optional
Pass your app id. timeoutMs bounds each call (30 seconds by default). The client authenticates
your app automatically, and every read and write is scoped to that app: an app cannot read or
write another app's data.
The SDK is separate from the Platform API (tRPC). The Platform API serves tenant business data; the SDK serves your app's own state.
The client implements the BextSdk interface:
| Namespace | Methods | Returns |
|---|---|---|
kv |
get<T>(key) · set(key, value, ttlSecs?) · delete(key) · list(prefix?, limit?) |
T | null · void · boolean · string[] (keys, at most 100 by default) |
db |
all<T>(sql, params?) · get<T>(sql, params?) · exec(sql, params?) |
T[] · T | null (first row) · { changes, lastInsertId } |
queue |
push(queue, payload, delaySecs?) |
the message id (string) |
realtime |
publish(topic, data) |
boolean |
email |
send({ to, subject, html?, text? }), with to an array of addresses |
boolean |
secrets |
get(key) |
string | null |
A few behaviours to know:
- An error response throws an
Error(the server's message, otherwise the HTTP status). Two exceptions:realtime.publishreturnsfalseandsecrets.getreturnsnullinstead of throwing. dbSQL parameters are sent as strings: a number becomes"42", andnullorundefinedbecomes an empty string. To write a realNULL, put it in the SQL.kv.setserializes the value to JSON andkv.getparses it back: pass the object as is, without callingJSON.stringifyfirst.
Each namespace has its own page: KV, Queue, Cache, Secrets and Entities for relational data. Long-running work goes to Tasks & Scheduler.
Vendoring npm packages
Pages render in an isolate, not in Node: built-ins such as node:crypto or node:fs are missing
or only partly stubbed. A library that depends on them will not work reliably at render time. The
isolate also refuses to turn strings into code: eval, new Function and string timers throw an
EvalError.
The dependable approach is to vendor the library's ESM build as .js files under a vendor/
directory in src/, and import them by relative path:
// @ts-ignore: no bundled .d.ts
import { sha256 } from "../vendor/noble-hashes/sha256.js";
- Pick pure-JS libraries with no Node built-ins and no
eval/new Function(for example@noble/hashes). - Add
// @ts-ignorewhen the vendored file has no.d.ts.
Where to next
| Page | What it covers |
|---|---|
| Routing & Rendering | The src/app/ route tree, metadata, ISR/SSR, streaming |
| Loaders & Actions | Data loading, mutations, request and session access |
| KV Store | Durable string store, TTLs, the double-encoding gotcha |
| Queue | At-least-once queue, HTTP handlers, dead-letter |
| Tasks & Scheduler | Long jobs with status and progress, recurring schedules |
| Cache | Caching recomputable values with a TTL |
| Secrets | App secrets and configuration read from environment variables |
| Entities | Admin screens with defineEntity, SQL queries with sql |