Praxy AI SDUI manual
Idea in one sentence
Server decides the UI. The client only renders trees or “deltas” the server sends—always after auth, tenancy, and policy checks. Everything below is framework‑agnostic: the only contract that matters is the HTTP abstraction (shape of the request + shape of the response).
Why it exists
- One front door — A single POST route (pattern:
/…/dispatch) normalizes payloads, errors, telemetry, and correlation ids. - Flat commands (“registry”) — Map an opaque
commandKeyto a deterministic handler so simple screens stay predictable. - Composite payloads — Sometimes you ship a tree of UI nodes (panels, timelines, forms) instead of many round-trips—the server composes once.
- Policy in one lane — Every path knows tenant (
company_idor your equivalent), roles, quotas, audit fields.
Nothing sensitive is inferred only in the browser: the server emits or withholds UI.
The HTTP contract (conceptual path)
Expose something like POST …/dispatch with JSON:
{
"commandKey": "resource.summary.card",
"payload": { "resourceId": "acct-4491" },
"context": { "locale": "en" }
}
Respond with an envelope your client already understands—usually a serialized SDUI tree or delta list plus metadata (correlationId, etc.). The exact envelope name is irrelevant; keep it consistent.
Auth and tenancy (plain words)
Authenticate (JWT, API key, etc.) → build a trusted context (who, tenant, scopes) → only then dispatch. On failure return structured errors — never leak half-built UI payloads.
Wiring the dispatcher – Python
Your web stack only needs a handler that validates input, attaches context, and calls registry/composite helpers. Minimal FastAPI-ish sketch:
@app.post("/api/sdui/dispatch")
def dispatch(payload: dict, tenant_ctx: TenantContext = Depends(resolve_tenant)):
body = validate_dispatch_body(payload)
return dispatch_sdui(
command_key=body["commandKey"],
payload=body.get("payload") or {},
context={**body.get("context", {}), **tenant_ctx.to_dict()},
)
Nothing here depends on a specific backend framework—you could swap routers, queues, Lambdas. The abstraction is validate → dispatch → sanitize → return.
Wiring the dispatcher – Node.js
Same flow over JSON on the wire:
async function dispatchHandler(req, res) {
const tenant = await resolveTenant(req);
const body = validateDispatchBody(req.body);
const envelope = await dispatchSdui({
commandKey: body.commandKey,
payload: body.payload ?? {},
context: { ...(body.context ?? {}), ...tenant.toJSON() },
});
res.json(envelope);
}
Express, Fastify, Hono, Bun—glue is interchangeable as long as the same contract is enforced.
Two ways to organize work
Registry commands (“one recipe per key”)
Caller sends commandKey + payload. The server maps that key to one logical handler; the handler returns deltas or trees your renderer understands.
Repeat the intuition with plain HTTP:
POST /api/sdui/dispatch
Content-Type: application/json
{
"commandKey": "resource.summary.card",
"payload": { "resourceId": "acct-4491" },
"context": { "locale": "en" }
}
Composite payloads (“bundles that move together”)
When UX needs choreography, assemble one tree (root id + ordered children) instead of chaining commands from the browser. Keep serialization order stable if you stream or paginate deltas.
Pick composites when sequencing matters more than minimal JSON depth.
Intent → command (optional hooks)
You may attach conversational routing: read turns → propose commandKey + payload fragments → invoke the dispatcher for auditable lineage. Hooks never replace policy—they only steer toward keys your registry already allows.
Authentication extras
Federated SSO, rotating API keys per tenant, hardware-bound secrets—merge resolved identity into telemetry (actor, scopes, resource types touched) before returning visible SDUI.
Happy-path sequence
- Validate credential → hydrate tenant/context
- Resolve registry handler vs composite builder
- Produce SDUI subtree/delta
- Policy filter nodes
- Return envelope + correlation id