ThinkingRoot Docs
Guides

Multi-tenant SaaS

Serve many of your own end-users under one project — isolated memory, per-user metering, and fair rate limits.

If you're building a product on ThinkingRoot — an app where your users each get their own memory — one project can serve all of them. Isolation, metering, and rate-limiting are per end-user and enforced at the gateway, so you don't have to provision a project per customer.

The whole model rests on one header: X-TR-User. A request that carries it is scoped to that end-user's namespace (u_{userId}); a request without it is an unscoped project call that reaches your shared/main workspace.

One credential, many users

You hold one tr_sk_… project key. You never mint a key per end-user — you name the end-user per request with X-TR-User. The gateway does the isolation.

The SDK does it by construction

The @thinkingroot/sdk splits the surface so the safe path is the default:

import { thinkingroot } from "@thinkingroot/sdk";

const tr = thinkingroot({
  gatewayUrl: process.env.TR_GATEWAY!,     // https://api.thinkingroot.com
  projectKey: process.env.TR_PROJECT_KEY!, // tr_sk_…
});

// Per-end-user memory — recall/store/capsule live ONLY on a scoped client.
const alice = tr.scope("alice");
await alice.store([{ statement: "Alice prefers dark mode" }]);
const hits = await alice.recall("what are Alice's UI preferences?");

// Project-level operations — branches, functions, flows, sources, streaming.
const ws = tr.workspace(); // defaults to "main"
const sources = await ws.sources();
  • tr.scope(userId) sends X-TR-User on every call and pins it to u_{userId}. There is no parameter to reach another user's data — a blank id throws (fail-closed).
  • tr.workspace(name) is unscoped (never sends X-TR-User) and operates on a named workspace (default main).

Isolation is enforced at the gateway

Even if a caller hand-rolls HTTP, the gateway's scope guard holds the line:

  • A u_* namespace is reachable only when X-TR-User authenticates as exactly that user. No header, or a mismatched one, is rejected.
  • The shared brain (main/shared) is readable by anyone, but a scoped user cannot write to it — promotion to shared is a separate, gated path.
  • A tr_sk_… key resolves to exactly one project's daemon, so cross-project access is impossible by construction; this guards cross-end-user access within your project.

See Tenant isolation for the full model.

Meter each end-user

Every metered (2xx) call is tagged with its X-TR-User. Break usage down per end-user — exactly what you need to bill or quota your own customers:

curl "$TR_GATEWAY/v1/projects/$PROJECT_ID/usage/by-user?days=30&limit=100" \
  -H "Authorization: Bearer $SESSION_JWT"
[
  { "user_id": "alice", "events": 1843, "units": 1843, "last_at": "2026-06-05T09:12:00Z" },
  { "user_id": "bob",   "events": 271,  "units": 271,  "last_at": "2026-06-04T22:40:00Z" }
]

Results are top-N by units (limit, max 1000) over the last days (max 365). Only end-user-scoped calls appear — unscoped project traffic has no end-user to attribute.

Fair rate limits

Rate limiting is per end-user within the project: a scoped request gets its own bucket ({project}#{user}), so one noisy end-user can't drain a shared allowance and starve the others. Unscoped project calls share the project bucket.

Every response carries the budget, and a 429 tells you exactly when to retry:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1780000000
Retry-After: 10

The SDK honors Retry-After automatically (bounded retries; configure with maxRetries, default 2):

const tr = thinkingroot({ gatewayUrl, projectKey, maxRetries: 3 });
// A transient 429 is retried with the gateway's Retry-After; a persistent one
// throws HttpError(429) so you can surface it.

Putting it together

A typical request flow for one of your end-users:

  1. Your backend authenticates your user (your auth, your session).
  2. It calls ThinkingRoot with your project key and X-TR-User: <that user> — or just tr.scope(userId) with the SDK.
  3. The gateway confines the call to u_{userId}, meters it under that user, and rate-limits that user's own bucket.
  4. You read /usage/by-user to bill or quota them.

No per-customer provisioning, no key sprawl, and isolation you can't forget to turn on.