@rexium-ai/app-sdk
v0.2.2
Published
Rexium App SDK — the escape hatch for genuinely-custom app UI. A typed client over OAuth 2.1 + PKCE → the Rexium MCP surface, with action methods GENERATED from the Spine ontology (writer=reader). Build any frontend, deploy it to {app}.rexium.app via the
Maintainers
Readme
@rexium-ai/app-sdk
The escape hatch for genuinely-custom app UI. When the declarative app
runtime (config-driven dashboards/tools) isn't enough and you want to build
any frontend, this SDK gives you a typed client over Rexium's OAuth 2.1 +
PKCE → MCP surface. Build with React/Vue/Svelte/anything, then deploy the
static bundle to {app}.rexium.app via the existing pipeline.
The action methods are generated from the Spine ontology — the SDK's typed surface is the ontology, kept in sync by codegen. (This is an ontology client, not a Palantir OSDK — it's our own thing.)
Install
npm i @rexium-ai/app-sdkRegister a PUBLIC (PKCE) OAuth app at build.rexium.ai/apps to get a
clientId, with redirect https://{your-slug}.rexium.app/ (or your dev URL).
Use
import { createClient } from "@rexium-ai/app-sdk";
const rx = createClient({
clientId: "rxa_…",
// scopes default to the full surface; narrow to least privilege:
scopes: ["commerce:read-orders", "commerce:write-products"],
orgId: "my-org-id",
});
// On app load: resumes a session, finishes the PKCE redirect, or starts one.
const session = await rx.connect();
if (!session) {
// We navigated to the consent screen; this page will reload with ?code=.
} else {
// Typed reads + writes over the ontology:
const stats = await rx.actions.getOrderStats({ days: 14 });
await rx.actions.adjustPrice({ productId: "p_123", newPrice: 1990 });
// Raw MCP escape hatch for anything not in the typed surface:
const data = await rx.call("GetStudioAppData", { appId: "a_1" });
// Inspect what your token can actually do:
console.log(await rx.listTools());
}rx.catalog lists every public action with its vertical / readonly flag /
required scopes — handy for building scope pickers or debugging grants.
Deploy to {app}.rexium.app
The SDK is just the client — hosting reuses the existing build pipeline:
npm run build # your app → dist/
# deploy dist/ via deployBuildApp (build.rexium.ai/apps "Deploy")No build worker of ours runs your code; we serve your prebuilt static bundle from R2, exactly like the reference Surfaces.
Regenerating the typed surface
src/generated.ts is produced from the live ActionDefinition catalog
(the agentInvokable subset). Re-run when actions change:
npm run build # at the repo root (builds deps)
node packages/app-sdk/scripts/codegen.mjsServer integrations (v0.2)
The browser client keeps a token in sessionStorage — fine for a dashboard,
useless for a backend that must keep sending for months. For that, use the
server profile: confidential client, refresh with rotation, single-flight
locking, and the error handling you would otherwise discover in production.
import { createServerClient, buildIdempotencyKey } from "@rexium-ai/app-sdk";
const rexium = createServerClient({
clientId: process.env.REXIUM_CLIENT_ID!,
clientSecret: process.env.REXIUM_CLIENT_SECRET!,
orgId: "your-org",
redirectUri: "https://your.app/auth/rexium/callback/",
store, // 3 methods — see below
});
// 1. connect (admin clicks a button, once)
const url = await rexium.authorizeUrl(["comms:send-whatsapp"]);
// …redirect the admin there; on the callback:
await rexium.exchangeCode(code, state);
// 2. use it forever
await rexium.call("SendWhatsApp", {
templateKey: "job-assigned",
kind: "transactional",
recipientPhone: "351912345678",
mode: "template",
templateName: "not_install",
languageCode: "pt_PT",
bodyVariables: JSON.stringify(["Ana", "12/03"]),
}, { idempotencyKey: buildIdempotencyKey("job", jobId, installerId) });The store (the only thing you write)
const store: AsyncTokenStore = {
load: async () => (await db.doc("system/rexium").get()).data() ?? null,
save: async (c) => { await db.doc("system/rexium").set(c, { merge: true }); },
clear: async () => { await db.doc("system/rexium").delete(); },
// Optional but STRONGLY recommended with more than one instance: refresh
// tokens rotate, so two concurrent refreshes invalidate each other.
withLock: async (fn) => db.runTransaction(async () => fn()),
};Three things that bite everyone (handled for you)
- Credentials go in the request BODY. Rexium's token endpoint ignores
Authorization: Basic, even though discovery advertises it. - Handler errors arrive as HTTP 200 with
result.isErrorand the message incontent[0].text. Checking onlyres.ok/body.errorrecords every rejected call as a success. - Refresh tokens rotate. Persist the new one or the connection dies — and never let two instances refresh at once.
