"Tibi"
Use Tether from a [Tibi](https://tibi.fi) app. Tibi has no npm dependency graph and no bundler, so unlike the Vue, React and Svelte SDKs there is no package to install. Instead, Tether ships as a first-party **Tibi marketplace plugin** that injects the client onto every page and exposes `window.Tibi.tether`.
The plugin mirrors the @tthr/client wire protocol (HTTP functions, WebSocket subscriptions with reconnect and catch-up, storage, auth) and adds component-scoped reactive helpers built on Tibi's plugin scope hook.
Requirements: Tibi >= 1.0.60 and tether plugin >= 0.2.0.
Installation
From your Tibi project:
tibi plugin add tether
This drops the packed plugin under plugins/tether/ and registers it in your .tibi-config. No npm install, no build step.
Configuration
The plugin is configured through environment variables, surfaced to the browser at render (or static build) time:
TIBI_TETHER_URL=https://tether.strands.gg
TIBI_TETHER_PROJECT_ID=your-project-id
TIBI_TETHER_PUBLISHABLE_KEY=tthr_pk_xxx
The publishable key is public by design. As with the other SPA SDKs, publishable keys can only call deployed functions; raw CRUD is blocked server-side.
If any required value is missing the client quietly disables itself, so enabling the plugin before Tether is provisioned is harmless.
User authentication
To send a signed-in user's JWT alongside the publishable key, tell the plugin where to find it:
# 'localstorage' or 'cookie'
TIBI_TETHER_AUTH_MODE=localstorage
# supports dot-notation into JSON localStorage values
TIBI_TETHER_AUTH_KEY=strands_oauth_session.accessToken
This follows the same AuthTokenSource semantics as the other SDKs. The resolved token is sent as Authorization: Bearer on HTTP calls and as a token query parameter on the WebSocket handshake. A callback source can be wired at runtime:
Tibi.tether.configureAuth({ mode: 'callback', callback: () => myToken });
Reactive helpers
Call these from inside a <script tibi> block (component or page). They mint Tibi state cells against the calling component and clean up automatically when the component unmounts.
useQuery
<template>
<ul>
<li t-for="todo of items" t-key="todo.id">{{ todo.title }}</li>
</ul>
</template>
<script tibi>
// Live by default: the initial fetch fills the cells, then server
// pushes keep them fresh over the WebSocket. The subscription is
// torn down automatically on unmount.
const todos = Tibi.tether.useQuery('todos.list', { completed: false });
const items = computed(() => todos.data.value || []);
</script>
useQuery returns { data, loading, error, refetch }. data, loading and error are state cells; refetch() re-runs the HTTP query. Pass { live: false } as the third argument for a fetch-once query with no subscription.
useMutation
<script tibi>
const createTodo = Tibi.tether.useMutation('todos.create', {
invalidates: ['todos.list'],
});
function add() {
createTodo.mutate({ title: title.value });
}
</script>
useMutation returns { mutate, loading, error, data, reset }. invalidates re-kicks matching live subscriptions after the write lands (exact name or dotted prefix).
useSubscription
<script tibi>
const feed = Tibi.tether.useSubscription('events.stream', {}, (snapshot) => {
// optional callback per push; feed.data is also a state cell
});
</script>
Imperative API
The full client is available anywhere as window.Tibi.tether:
// Query (read-only)
const rows = await Tibi.tether.query('todos.list', { completed: false });
// Mutation, with optional invalidation
await Tibi.tether.mutation('todos.create', { title: 'Buy milk' }, {
invalidates: ['todos.list'],
});
// Action (side-effects that are not pure writes)
await Tibi.tether.action('emails.send', { to: '[email protected]' });
// Manual subscription; returns an unsubscribe function
const off = Tibi.tether.subscribe('todos.list', {}, (rows) => {
// fresh snapshot on every server push
});
// Re-kick live subscriptions by name
Tibi.tether.invalidateQueries(['todos.list']);
The transport is one multiplexed WebSocket per page with heartbeat, exponential-backoff reconnect and last_sync_time catch-up, matching @tthr/client.
File storage
// Upload with progress; resolves to the asset id
const assetId = await Tibi.tether.storage.upload(file, {
onProgress: ({ loaded, total }) => { /* update a progress bar */ },
});
const url = await Tibi.tether.storage.getUrl(assetId);
const meta = await Tibi.tether.storage.getMetadata(assetId);
await Tibi.tether.storage.delete(assetId);
const page = await Tibi.tether.storage.list({ limit: 20 });
Uploads go directly to storage via a presigned URL, then confirm with the Tether API, exactly like the other SDKs.
SSR and static builds
Both of Tibi's render paths ship the plugin:
tibi serveinjects the client and configuration per request.tibi build(from Tibi 1.0.60) bakes the client and a snapshot of theTIBI_TETHER_*environment into the static runtime bundle, so a statically deployed site behaves identically. Set the environment variables in your build environment (CI, Cloudflare Pages build settings, etc.).
For server-rendered data at request time, use Tibi frontmatter with a server API key against the Tether HTTP API directly; the browser plugin is intentionally publishable-key only.
Differences from the npm SDKs
- No
TetherProvideror app-level plugin registration; configuration is environment-driven. - Function references are dotted string names (
'todos.list'), not generatedapi.*proxies. - The bridge surface (
ctx.bridge) is server-side only and not exposed in the browser client. - Execution logging is not shipped from the browser client yet.