Soba Docs

Integrate

Identifying users

Every Soba call takes a user id. It is opaque to us, so picking one that outlives your database is the one thing we cannot do for you.

Every Soba call takes a user. It is how a run is attributed to a person, therefore how it is metered, therefore how Soba knows whose machine it may reach. Soba keys an end user on (app, user, environment) and nothing else.

The string is opaque to us on purpose — whatever you send names a person, and a string we have never seen names a person we have never met. That is the right behaviour, and it is also the one place an integration can go quietly wrong, so this page is worth five minutes before you ship.

Three rules#

Resolve it on the server, from the session. Never accept a user id from the browser. If you do, anyone can claim to be anyone and run on their machine.

Send the same id everywhere. The run, the connect session, the plans list, the setup call. The person who pairs a laptop and the person a run is attributed to have to be the same row, or one of them pairs a machine the other cannot reach.

Pick an id that outlives your database. This is the one that is easy to get wrong while following the other two, and the only one with no symptom until much later.

Why the third rule exists#

session.user.id is the obvious thing to send, and in most apps it is a random key minted at sign-up and stored in a row. It is stable exactly as long as that row is.

Reset the database, migrate it, or stand up a second instance of your app, and the same person signs in, is issued a fresh key, and arrives at Soba as somebody new. The laptop they paired last week stays attached to the id they no longer have. Your Users page then shows them twice: once holding the machine, once on metered compute with no runs.

Soba cannot detect this. email travels with a session, but it is a label on that page, not an identifier — two ids carrying one address are two people, drawn on two rows under one name. From here, a new id is a new person, and that is the only thing it can be.

The damage is not cosmetic. Your user count is inflated, your connect rate is divided by however many ids each person has accumulated, and the machine a person connected is attached to an id your app will never send again — so their next run routes to metered compute while their own hardware sits idle.

Deriving a stable id#

If your own ids are stable — a username, an immutable account number, a tenant id you have never reassigned — send them and ignore the rest of this page.

If they are not, derive one from something that is:

JavaScript
import { Soba } from "@soba-so/sdk";
import { sobaUserId } from "@soba-so/sdk/identity";

const soba = new Soba({ apiKey: process.env.SOBA_KEY });

const user = sobaUserId(session.user.username);   // stable across resets

const run = await soba.run({ user, prompt: "Summarise my week" });

sobaUserId hashes the input and returns usr_ plus twelve hex characters. It normalises case and surrounding whitespace, and nothing else — folding anything further would be a guess about what your app considers one account, and a wrong guess merges two people, which is worse than splitting one and cannot be undone.

It lives on a subpath, @soba-so/sdk/identity, rather than the main entry. That is deliberate: it is server-only, so importing it in client code should fail at your bundler rather than in production.

Pick the input carefully. It must be unique per person and must never be reassigned. A username or a verified email address is usually right. A display name is not, and neither is an address people can change to one another's.

Use the same call for the connect session:

JavaScript
// app/api/soba/session/route.ts
const res = await fetch("https://api.soba.so/v1/end_users/session", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.SOBA_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ user: sobaUserId(session.user.username) }),
});

Has it already happened?#

Your Users page says so. When more than one id carries the same label and at least one of them has no machine and no runs, the page tells you, names the ids, and so does the overview above it.

It is a hypothesis, not a finding — two colleagues genuinely sharing an address look identical from here — so nothing is merged automatically and no figure is adjusted. If those ids are one person, switch to a derived id first and deploy it, then get in touch to have the rows reconciled. Order matters: while your app is still sending the old ids, merging the rows only makes room for them to come back.

Environments are part of the identity#

The same user string under a development key and a production key is two people, deliberately. See Environments.

Privacy · Terms · © 2026 Soba resolved = machine grant ∩ broker request