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/*"] }
  }
}
Note

.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.publish returns false and secrets.get returns null instead of throwing.
  • db SQL parameters are sent as strings: a number becomes "42", and null or undefined becomes an empty string. To write a real NULL, put it in the SQL.
  • kv.set serializes the value to JSON and kv.get parses it back: pass the object as is, without calling JSON.stringify first.

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

Warning

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-ignore when 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