Overview
Every Inklura app — the console, SMS Designer, SEO, the mail composer, and the tenant
sites — is a bext PRISM app. PRISM is two things: a server-rendering framework
(@bext-stack/framework) and a platform SDK exposed to each app over a trusted
loopback interface.
- Framework — a zero-runtime JSX→string engine. There is no React, no virtual DOM, and no client runtime by default. A component is just a function that returns a string.
- SDK — KV, queue, tasks, cache, secrets, a scheduler, and relational entities, reached
at
http://127.0.0.1/__bext/sdk/*and authenticated by an app id.
What PRISM is
A PRISM component is a plain function:
type Component = (props) => string | Promise<string> | AsyncIterable<string>;
It returns a string (or a promise/async-iterable of strings, for streaming). JSX compiles straight to string concatenation — 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) use a different pragma plus a directive:
/** @jsxImportSource @bext-stack/framework/signals */
"use signals";
Site layout
A PRISM site is a workspace 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/ # root-owned, gitignored compiler cache
package.json depends on the framework as a workspace package:
{
"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 a root-owned, gitignored compiler cache. Never 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 SDK loopback model
The platform SDK is a set of HTTP endpoints under http://127.0.0.1/__bext/sdk/*. Apps do
not use an admin JWT to reach them. Instead, an on-host app sends its app id:
X-Bext-App-Id: <app-id>
This header is only trusted from loopback (127.0.0.1 / ::1). Private and Docker IP
ranges are not trusted. When present from loopback, it skips the admin-JWT requirement
and scopes all data to that app id — an app cannot read or write another app's data.
The same origin also serves the Platform API (tRPC). The SDK endpoints here are a separate, app-scoped surface, not the public API.
The createSdk client
The framework ships a JS client so you rarely touch raw HTTP:
import { createSdk } from "@bext-stack/framework";
const sdk = createSdk("<app-id>");
// base http://127.0.0.1/__bext/sdk, 30s timeout
The client implements the BextSdk interface:
| Namespace | Methods |
|---|---|
kv |
get<T>(key) · set(key, value, ttlSecs?) · delete(key) · list(prefix?, limit?) |
db |
all<T>(sql, params?) · get<T>(sql, params?) · exec(sql, params?) |
queue |
push(queue, payload, delaySecs?) |
realtime |
publish(topic, data) |
email |
send({ to, subject, html?, text? }) |
secrets |
get(key) |
Each namespace has a dedicated page: KV, Queue, Cache, Secrets, and Entities (relational data). Long-running work goes to Tasks & Scheduler.
Vendoring npm packages (caveat)
The PRISM bundler does not resolve bare npm specifiers into the render isolate, and the
isolate has no Node built-ins (node:crypto, etc.). A bare require("pkg") /
import "pkg" for an unvendored package fails.
Workaround: vendor the package's ESM build as relative-imported .js files under
src/**/vendor/ and import them by relative path:
// @ts-ignore — no bundled .d.ts
import { sha256 } from "../vendor/noble-hashes/sha256.js";
- Prefer pure-JS libraries with no Node built-ins (for example
@noble/hashes). - Add
// @ts-ignorewhen the vendored file has no.d.ts. - Beware tree-shaking: an exported top-level
constarray that is only read indirectly can be shaken away. Export a named accessor function instead of the bareconst.
Where to next
| Page | What it covers |
|---|---|
| Routing & Rendering | The src/app/ route tree, metadata, ISR/SSR, streaming |
| Loaders & Actions | Data loading, mutations, request access, readSession |
| KV Store | Durable string store, TTLs, the double-encoding gotcha |
| Queue | At-least-once push queue, workers, dead-letter |
| Tasks & Scheduler | Warm task-executor for long jobs, cron scheduler |
| Cache | TTL scratch cache |
| Secrets | Secret store, also exposed as env vars |
| Entities (Neon) | defineEntity admin CRUD + direct Neon access |