Tether
DocsGitHub

Guides

Scoping multi-site tenants

Most customers are not one place. A retailer has forty stores, a clinic group has twelve sites, an agency has clients. X-Tether-Scope-Id is how a turn is narrowed to one of them.

The model

tenant (API key)          the customer — Northside Group
 └── scope (header)       one site, ledger, region, workspace — Store #9
      └── user (header)   the person acting — Ana, shift lead

One key per customer, not per site. The scope arrives per request, which means a single integration serves every site your customer has, and a user switching sites is a header change rather than a new credential.

Setting it server-side

// Your backend, not your browser.
async function ask(session, prompt) {
  return fetch('https://api.trytether.ge/v1/chat', {
    method: 'POST',
    headers: {
      'X-Api-Key': process.env.TETHER_SECRET_KEY,
      'X-Tether-Scope-Id': String(session.activeSiteId),   // from YOUR session
      'X-Tether-User-Id': String(session.userId),
      'X-Tether-User-Role': session.role,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ prompt }),
  })
}

Which tools should demand a scope

Register requires_scope: true on any tool whose answer is misleading when aggregated:

Toolrequires_scopeWhy
get_open_ticketstrue"You have 41 open tickets" across twelve sites helps nobody
get_stock_levelstrueStock is physical and lives in one place
get_monthly_spendfalseThe group-wide figure is a real, useful number
list_sitesfalseAnswering it is how the model learns what the scopes are

With the flag set, a scopeless turn makes the model ask which site rather than guessing. Enforce it in your endpoint too — the flag shapes model behaviour; your endpoint is what actually refuses:

if (toolRequiresScope(tool) && !tenant.scope_id) {
  return res.json({ error: 'This needs a specific site. Ask the user which one.' })
}

The model reads that message and asks. That is the intended behaviour, not a failure.

Cross-site questions

"Which store has the most open tickets?" cannot be answered by a per-site tool.

Give it its own tool, explicitly org-wide, and say so in the description:

{
  "name": "compare_sites_open_tickets",
  "description": "Open ticket counts for every site in the organization, for comparison. Use when the user asks which site is busiest or wants a cross-site comparison. For a single site, use get_open_tickets instead.",
  "domain": "orders",
  "is_write": false,
  "requires_scope": false,
  "parameters": { "type": "object", "properties": {}, "required": [] }
}

A tool that quietly ignores scope_id and answers org-wide is the shape to avoid — the model cannot tell the two apart, and the user gets a group total when they asked about their store.

Writes and scope

A staged write records the scope it was created under, and that scope is what reaches your endpoint at execution — not whatever scope the confirming request happened to carry.

At confirm time the tenant and the staging user are re-checked — a confirm carrying a different X-Tether-User-Id returns NotFound. The scope is not re-checked: the same user can confirm an action they staged under any of that tenant's scopes.

If a store manager must not approve another store's writes, enforce that in your endpoint, where you know your org chart:

if (isWrite && !(await mayActOnSite(tenant.user_id, tenant.scope_id))) {
  return res.json({ error: 'You can only approve actions for your own site.' })
}

Scope changes mid-conversation

A session belongs to a tenant and a user, not to a scope. Switching sites while a thread is open means the earlier turns in that thread were about a different place.

The cleaner behaviour is to start a new session when the active site changes:

function onSiteChange(newSiteId) {
  session.activeSiteId = newSiteId
  session.assistantSessionId = null   // next turn starts a fresh thread
}

Insights per site

Insights is scoped the same way. For a per-site digest, call once per site with the scope header set — and remember each call is a full billable turn, so a nightly digest across forty sites is forty turns against the same budget caps.

Memory and scope

Preferences are stored per tenant and user, not per scope. A preference learned at one site follows the person to another, which is usually right for "answer concisely" and wrong for "default to store 9".

Keep scope out of remembered preferences and let the header carry it.