Witbitz docs HomeTrustAll docs

Your server — WitbitzAdmin

What belongs to your tenant rather than to a room — its keys, the origins your pages run on, your users and what they cost, deleting rooms — is done from your server with WitbitzAdmin, never from a page.

Part of the SDK · every call: the reference.


Set it up

Most of the SDK is a member's work and needs no key: a room proves itself by its key, so the room endpoint asks for nothing else. A few calls are the builder's — your OAuth apps, the hosts your users may attach a key for, the origins your pages run on, deleting rooms, billing — and those belong to your tenant, proved by your tenant secret key (wsk_…). They live in their own module, for your server: a page holding the key has given it away, and a page that imports only witbitz-sdk.js cannot hold it by accident.

Download it once, as one file, beside your server's code (Node cannot import from a URL):

shell
curl -O https://witbitz-spaces.pages.dev/sdk/3.10.0/witbitz-admin.mjs   # a released version; Node 18+, no packages
javascript
import { WitbitzAdmin } from './witbitz-admin.mjs'
const admin = new WitbitzAdmin({ tenant: 't_…', tenantKey: process.env.WITBITZ_TENANT_KEY })
const { ok, tenant, reason } = await admin.verifyTenantKey()   // is this key good, and whose is it?

Restricted keys

Your secret key can do everything, including deleting every room and the tenant itself. A server that needs only part of that — an account server that creates customers and mints their tokens — should hold a restricted key (wrk_…) with only the capabilities you choose. Mint one with your secret key:

javascript
const { key, keyId } = await admin.keys.create({
  type: 'restricted', label: 'account server',
  scopes: ['customers:write', 'customers:tokens'],
  maxCustomerCapUsd: 50,     // optional: the highest monthly cap this key may give a customer
  expiresAt: '2027-10-01',   // optional: refused from then on; at most two years ahead
})
const accounts = new WitbitzAdmin({ tenant: 't_…', tenantKey: key })   // the same client, fewer powers

key is shown once: store it as that server's secret, and keep the wsk_ key offline.

Capability What it opens
customers:read admin.customers.list()
customers:write customers.create, customers.update, customers.revoke
customers:tokens customers.token(userId) — a customer's turn token
customers:erase customers.erase({ email }) — deletes the rooms that person created
rooms:read · rooms:write · rooms:delete your room counts · refreshing and restoring rooms · deleting one room
settings:read · settings:write your tenant record · the webhook URL, requireUserToken, visitorLimit, your app's name, provider keys
origins:write admin.origins.declare — which pages act as your app
integrations:write admin.providers and admin.hosts
billing:read · wallets:grant admin.billing · granting credit
apps:release releasing your app through /v1/apps (witbitz app release): reserve, submit and read a release — not claiming a new app id

Signing your tenant's requests

Your tenant key proves who you are to Witbitz's gateway, and the Sentinel — the enclave that holds your OAuth apps' secrets, your users' connections and their erase lists — would otherwise take the gateway's word for it. Give your server a signing key and the Sentinel checks you itself: every request your server sends it is signed, and once the key is registered, an unsigned or altered request for your tenant is refused, whoever sends it, Witbitz's own servers included.

javascript
const { privateKey, publicKey } = await WitbitzAdmin.generateSigningKey()   // once; keep privateKey with your tenant key
const admin = new WitbitzAdmin({ tenant: 't_…', tenantKey: process.env.WITBITZ_TENANT_KEY, signingKey: privateKey })
await admin.signing.register()   // from now on, every Sentinel request of this tenant must be signed

Your page origins

The origins your pages run on act for your tenant: a room created from one of them is yours, and your rooms can be read and embedded from them. A page on an origin you have not declared gets 403 tenant_required when it creates a room.

javascript
await admin.origins.declare([
  'https://app.example.com',
  'https://test.example.com',
  'https://old-box.example.ts.net',   // declaring replaces the list: keep every origin you still use
])   // → { origins, unproven }
await admin.origins.list()

Prove the origin is yours

Declaring an origin is enough for your pages to work. For your app's name to appear in sign-in mails sent from it, the origin's host must say it belongs to your tenant, one of two ways:

We check when you declare: declare answers each origin it could not prove in unproven, with the exact file or record to add — add it and declare the same list again. GET /v1/tenant lists the proven ones in origins_proven. Without this, anyone could declare a site that is not theirs and have Witbitz mail its users a code in that site's name.

Your app's name

A sign-in code mailed from one of your declared and proven origins comes from " via Witbitz", with the subject "Your code: 123456", and names the site that asked — so your users find it when they look for your app.

javascript
await admin.app.rename('Keepwarden')   // → { name: 'Keepwarden' }
await admin.app.name()                 // → 'Keepwarden'

Hosts and OAuth apps

javascript
await admin.hosts.declare(['api.example.com'])   // the hosts your users may attach a key for; replaces the list
await admin.hosts.list()
await admin.providers.attach({ kind: 'google', clientId, clientSecret, redirectUri, scopes, label })   // → { id, kind, tenant }
await admin.providers.attachMcp({ issuer, clientId, clientSecret, redirectUris, label })
await admin.providers.list()
await admin.providers.revoke(id)   // → { revoked, connections }

The client secret of an OAuth app you attach is sealed to the Sentinel. Your users then connect through it from the page — connections. attachMcp files an OAuth app for an MCP server that does not let apps register themselves (GitHub, Asana, Box).

Billing, per user

Your rooms' model use is billed to you, in the provider's own numbers per key — never our token arithmetic. To see, cap and pass on what each of your users costs, give each one their own key: their turns then bill to it, and what they cost is what the provider reports for that key.

javascript
// on your server, once per user
const user = await admin.customers.create({ userRef: 'cust-42', capMonthlyUsd: 10 })   // → { userId: 'u_…', … }
// on your server, when that user signs in, and again before it expires
const { token, expiresAt } = await admin.customers.token(user.userId, { ttlSec: 900 })
javascript
// in their page
const wb = new Witbitz({ tenant: 't_…', userToken: token })
wb.useUserToken(async () => (await fetch('/api/witbitz-token')).text())   // or a function that fetches a fresh one

People who are not signed in

A public page — a help desk, a demo, a support chat — has visitors with no account and so no user token. Their questions bill to your tenant key. To keep that bounded, set how many questions a day they may ask:

javascript
await admin.billing.visitorLimit({ perNetwork: 20, total: 2000 })   // null turns it off

What you are billed, from your server — the same numbers as the tenant portal:

javascript
const b = await admin.billing.get()                    // { month, days, users, userDays, storage, rate, … }, in microdollars
const csv = await admin.billing.csv({ byUser: true })   // one line per user per day; without it, one line per day

month is your own key, live, plus your users' keys through yesterday; users lists each with its cap and this month's spend. Over HTTP: POST|GET /v1/tenant/users, PATCH /v1/tenant/users/{id} { "capMonthlyUsd": … }, DELETE /v1/tenant/users/{id}, POST /v1/tenant/users/{id}/token { "ttlSec": … }, PUT /v1/tenant/webhooks { "requireUserToken": true }, and GET /v1/billing (?format=csv, &by=user), each with your secret key.

Knowing who is signed in, on your server

Before your server mints a user token, it should know the caller really is the Witbitz account it claims. A signed-in page holds wb.session = { email, idToken, exp, sign }, and idToken is a standard JWT your server can verify on its own:

Deleting a room

When a person deletes their data, delete their rooms from your server. You choose how much to keep, and for how long:

javascript
await admin.rooms.delete('sp-…')                                     // retain: 'none' — everything, now
await admin.rooms.delete('sp-…', { retain: 'backups' })              // now, except old versions for 35 days
await admin.rooms.delete('sp-…', { retain: 'grace', graceDays: 30 }) // nothing yet; undo until the window ends
await admin.rooms.restore('sp-…')                                    // → { restored: true, room }
retain The room Its data Undo
'none' (default) gone at once every stored version deleted now: sealed messages, files, the confidential tier's copies none
'backups' gone at once current copies deleted now; old versions expire within 35 days only by Witbitz operators, by hand, until then
'grace' gone at once nothing deleted until the window ends (graceDays, 1–90, default 30), then purged as after says ('none' by default, or 'backups') admin.rooms.restore(room) until the window ends

"Gone at once" holds in every mode: from the moment you call, the room answers 410 deleted to everyone, a member's open device included, and no one can make a room with the same id again. A member's space.send returns { status: 410, error: 'deleted' }.

Your rooms, and the idle rule

admin.rooms.list() returns your rooms a page at a time — to find a room whose link was lost, or to audit what you hold:

javascript
let cursor
do {
  const { rooms, next } = await admin.rooms.list({ cursor })   // rooms: [{ room, createdAt, lastUsedAt, kind, split, gated, held }]
  cursor = next
} while (cursor)

createdAt and lastUsedAt are ISO times or null. lastUsedAt is when a member last opened the room, wrote to it or prompted in it (recorded since 2026-10-07; your own rooms/touch does not count). held is { purgeAt } for a room waiting to be deleted. Rooms made before creation times were recorded show createdAt: null.

The idle rule deletes rooms nobody uses, on a schedule you set:

javascript
await admin.rooms.idleDeletion(180)    // rooms no member used for 180 days: held, then deleted
await admin.rooms.idleDeletion(null)   // off

Once a day, each room past the limit is put on a 30-day hold — it answers 410 at once, and admin.rooms.restore(room) brings it back until the hold ends — and is then deleted (retain: 'grace' above). The limit is 30 to 3,650 days. A room with no recorded use counts from 2026-10-07, so turning the rule on deletes nothing that day. At most 100 of your rooms are held a day. Like delete, it cannot know who else uses a room — but a room past the limit is one nobody has opened.

Erasing a user who has left

When someone deletes their account in your app, you want their rooms gone too. You cannot simply delete "their rooms" from your server: you have no list, membership is sealed, and a room they shared belongs to the others in it. Erasure works from a list the person's own devices filed while they used your app, so neither you nor Witbitz holds it before:

javascript
// on your server, with your tenant key — after the person asked to be erased
const r = await admin.customers.erase({ email: 'dana@example.com' })
// → { filed, rooms, deleted, shared, notTheirs, failed, cleared, incomplete? }
if (r.failed > 0 || r.incomplete) { /* run it again later; what was deleted stays deleted */ }
await admin.customers.revoke(userId)   // their billing key, if they had one

How it holds. Three things in the page make it safe, and wb.erasure does all three for you:

  1. A deletion commitment. Every room the SDK creates records a hash of a delete secret, derived from the account's backup master secret and the room id — so only that account can produce it, on any of its devices. A person who joined holds the room key but not the secret, so cannot delete the room.
  2. A membership mark. Every room the SDK opens is marked with an opaque tag for the account. A room that carries anyone else's mark is refused as shared, even when its creator asks.
  3. The filed list. A few seconds after a room is created or opened, the account's list of rooms and delete secrets is sealed to Witbitz's attested enclave under your tenant and the person's email, each filing replacing the last. It is opened only when your server calls erase, and cleared once nothing failed.

What the page needs: new Witbitz({ tenant }), a signed-in person (wb.signIn) and an enabled wb.backup — the backup's master secret is what every device of the account derives the same delete secrets from, and the enclave keeps one list per account.

javascript
await wb.erasure.file()      // file now, e.g. right after a sign-in or a restore (it also runs by itself)
await wb.erasure.status()    // → { filed, rooms, updated }
wb.erasure.available         // true when this page can file: a tenant, a sign-in and a backup

Machine-readable source: sdk-builder.md · Generated 2026-10-27T07:05:42Z · build f301388c · every doc in one fetch: llms-full.txt (HTML)