"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 serve injects the client and configuration per request.
  • tibi build (from Tibi 1.0.60) bakes the client and a snapshot of the TIBI_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 TetherProvider or app-level plugin registration; configuration is environment-driven.
  • Function references are dotted string names ('todos.list'), not generated api.* 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.