SDKs: Go backends, node/python patterns, and the in-frame JS API

August 24, 2026 · View on GitHub

Go SDK — github.com/xbin-dev/xbin/sdk

Zero-dependency. In a xbin workspace the generated go.work resolves it (the container ships the module at /opt/xbin/sdk); just require it:

import xbin "github.com/xbin-dev/xbin/sdk"

Serving

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /events", list)
	mux.Handle("POST /events", xbin.RoleFunc("writer", create))
	xbin.Serve(mux) // listens on XBIN_SOCKET, drains gracefully on SIGTERM
}

Your handler sees paths with the /api/<component> prefix already stripped. xbin.Self() returns your component path.

Callers and roles

c := xbin.Caller(r)          // CallerInfo{From, Role, Owner, User, UserLevel}
c.UserCanWrite()             // gate mutating endpoints on the DRIVING user's
                             // level (D29) — frame calls from your own UI run
                             // at full role even for read-level viewers
xbin.Role("writer", h)       // middleware: 403 below writer
xbin.RoleFunc("writer", hf)  // same, for HandlerFuncs
xbin.RoleSatisfies(have, want) // admin ⊃ writer ⊃ reader; custom = exact
c.Ingress()                  // anonymous PUBLIC traffic via a published
                             // endpoint (docs/ingress.md) — no role; the
                             // public hostname is in X-XBin-Ingress-Host

Headers are trustworthy: xbind strips inbound X-XBin-* and injects verified values (auth.md).

Calling other elements & xbin APIs

resp, err := xbin.Client().Get("http://xbin/api/apps/calendar/events?day=2026-07-02")

xbin.Client() routes through the gateway socket with this generation's instance credential. The host is always the literal xbin. 403 means a missing grant — declare it in uses, get it approved.

Long-running calls stream. The client has no overall timeout: SSE / chunked responses from another element run until either side closes (bound individual calls with a request context). For WebSocket to another element, dial any WS library through the gateway:

d := websocket.Dialer{NetDialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
	return xbin.GatewayDial(ctx)
}}
h := http.Header{"Authorization": {"Bearer " + os.Getenv("XBIN_TOKEN")}}
conn, _, err := d.DialContext(ctx, "ws://xbin/api/apps/other/stream", h)

Remember the lifecycle: streams to a backend die at its blue/green drain (30 s after a save there) — reconnect loops are mandatory. Backends serving active streams are not idle-reaped.

Resources, vault, bus

kv := xbin.KV(xbin.Resource("events"))      // resources.md for the full KV API
path := xbin.Resource("db")                  // sqlite file path (same-scope)
secret, err := xbin.Secret("imap-pass")      // own vault
err = xbin.Publish(xbin.Resource("bus"), "events/created", ev)

xbin.Resource(name) reads XBIN_RES_<NAME>; empty string = not granted.

node backend (no SDK needed)

The contract is just "HTTP on a unix socket + a couple of env vars", so a library is optional. bx new --runtime node scaffolds:

const http = require('http');
const srv = http.createServer((req, res) => {
  const caller = req.headers['x-xbin-from'];   // verified
  const role   = req.headers['x-xbin-role'];
  res.setHeader('content-type', 'application/json');
  res.end(JSON.stringify({ hello: caller }) + '\n');
});
srv.listen(process.env.XBIN_SOCKET);
process.on('SIGTERM', () => srv.close(() => process.exit(0)));

Calling out through the gateway:

const { request } = require('http');
const req = request({
  socketPath: process.env.XBIN_GATEWAY,
  path: '/api/apps/calendar/events',
  headers: { authorization: `Bearer ${process.env.XBIN_TOKEN}` },
}, handleResponse);
req.end();

python backend

bx new --runtime python scaffolds a UnixStreamServer + BaseHTTPRequestHandler skeleton. Gateway calls: any HTTP client that supports unix sockets (requests + requests-unixsocket, or raw http.client.HTTPConnection with a connected socket), bearer token from XBIN_TOKEN. For quick scripts consider runtime: cgi instead — env in, stdout out, nothing to keep alive.

cgi backend

Any executable. CGI/1.1 env (PATH_INFO, QUERY_STRING, REQUEST_METHOD, body on stdin) plus XBIN_COMPONENT, XBIN_FROM, XBIN_ROLE. Response: headers, blank line, body on stdout. Perfect for shell-script endpoints; one exec per request (no persistent process, so no idle reaping or drain).

In-frame JS API (window.xbin)

Injected into every component document via xbin-client.js (unless the manifest sets inject: false). No imports needed.

xbin.self                       // "apps/thing" — this component's path

// a bound http interface (plans/interfaces.md): { url, service } or null. Call a
// typed, swappable dependency instead of hard-coding a path — the owner binds
// which provider satisfies it (bx bind / admin Interfaces tab), and the binding
// is also the call grant.
const llm = xbin.iface('llm');  // { url: '/api/apps/llm-gw', service: 'openai' }
if (llm) await xbin.fetch(`${llm.url}/v1/chat/completions`, { method: 'POST', … });

// fetch with identity attribution. REQUIRED for calling other elements'
// APIs from the browser: it attaches the frame token so the callee sees your
// tile as the caller. A plain fetch to a sibling is unattributed — 403 for a
// non-admin user; an admin's own cookie would call as admin and mask a missing
// grant, so always use xbin.fetch (auth.md). Streaming (SSE) works.
const r = await xbin.fetch(`/api/${xbin.self}/events`);
const r2 = await xbin.fetch('/api/apps/calendar/events'); // needs a grant

// attributed WebSocket to an element API (browsers can't set WS headers,
// so the frame token rides a query param xbind consumes — the callee
// never sees it):
const sock = xbin.ws('/api/apps/other/stream');

// attributed URL string — same query-param trick for TAG-driven requests
// (<a href>, media src) that can't set headers. Build at click time: the
// embedded token is short-lived. The classic use is a backend-streamed
// download (endpoint answers Content-Disposition: attachment):
a.href = xbin.url(`/api/${xbin.self}/export.csv`);

// client-side file download (sandboxed tiles may download — allow-downloads,
// ND10). data: Blob | ArrayBuffer | TypedArray | string. Call from a user
// gesture; browsers throttle unprompted downloads.
xbin.download('report.csv', csvText, 'text/csv');

// bus (needs a reader grant on the resource)
const off = xbin.bus.on('res:apps/thing/bus/events/', (topic, data) => {…});
await xbin.bus.publish('res:apps/thing/bus', 'events/created', ev); // writer

// raw event stream: reload / build-start / build-error / build-ok / bus / grants
const off2 = xbin.events.on((e) => {…});

// ---- dialogs & pop-out windows (a tile is an iframe → the SHELL spawns these
// over the whole workspace; see docs/elements.md §Dialogs & windows) ----

// A trusted modal rendered by the shell from plain data (no markup — safe).
// Resolves { button, values }: button = the clicked button's value (null if
// dismissed via Esc / backdrop / Cancel), values = the field inputs.
const res = await xbin.dialog({
  title: 'Delete account?',
  message: 'This removes apps/imap#aurora and its stored mail.',
  fields: [{ name: 'confirm', label: 'Type the name to confirm', placeholder: 'aurora' }],
  buttons: [{ label: 'Cancel', value: null }, { label: 'Delete', value: 'del', danger: true }],
});
if (res.button === 'del' && res.values.confirm === 'aurora') { … }

// A floating window running YOUR OWN UI: it frames a sub-path of this component
// (a normal tile document), so it has its own xbin client and talks to your
// backend with the usual xbin.fetch. Escapes the tile's clipping.
const win = xbin.window({ path: 'compose', title: 'New message', width: 620, height: 440 });
win.closed.then(() => refresh());   // resolves when the window is closed
// win.close() to close it yourself. spec.src frames a full component path
// instead of a sub-path (subject to the same tile-access RBAC as <bx-frame>).

xbin.dialog falls back to an in-frame modal when the tile isn't embedded in the shell; xbin.window needs the shell (no-op otherwise).

Height reporting to the embedding <bx-frame> and frame-token refresh are automatic.

Core elements (import in main-document pages like root/index.html)

<script type="module">
  import '/vendor/bx-frame.js';     // <bx-frame src="…">
  import '/vendor/bx-terminal.js';  // <bx-terminal cwd="…"> (bx-frame uses it)
  import '/vendor/bx-grants.js';    // <bx-grants> owner approval panel
  import '/vendor/bx-dialog.js';    // <bx-dialog> modal (xbin.dialog fallback)
</script>

lit is importable everywhere via the injected import map (import { LitElement, html, css } from 'lit') — vendored, no CDN, works offline.