sk_ and
hands to one actor — a signed-in person’s browser or a scoped agent. It’s
the same primitive in both cases (backend-minted, short-lived, scoped); the only
difference is the subject and how much authority it carries.
One resource mints both:
Your backend (sk_)
user mints an ek_ (ephemeral key); agent mints an rk_ (restricted key).
You pass user or agent — never both.
It exists because of one rule: the browser can never hold a secret. Your
sk_ lives on the server; the browser only ever holds a minted session token
(which already names your org). So the per-actor credential is minted
server-side, scoped, and expires in minutes — the model Stripe uses for
client-side SDKs.
Why
Ablo doesn’t authenticate your users — you do, however you like (your own sessions, an IdP, anything). Ablo authenticates your project (thesk_ that
minted the session) and trusts the identity you asserted at mint time. The
session token is that assertion: “this connection is acting as U, in org
O, until it expires.”
End-user sessions (ek_)
For a logged-in person using your app. Mint on a backend route that has already
authenticated the user:
Your backend route (session-authed)
Ablo client whose getToken fetches from that route, and pass
the instance to <AbloProvider>. The client fetches the token,
opens the connection, and re-mints before expiry — your app writes no token
plumbing:
<Elements stripe={...}> model).
Build the client once at module scope — a new instance per render tears down
the socket. authEndpoint: '/api/ablo-session' is accepted as sugar for the
getToken fetch above if you prefer a URL.
Agent sessions (rk_)
For a non-human actor — an agent or automation that should only do specific
operations. The can map is the permission boundary, and it’s typed against
your schema — the model keys are your schema’s models, so a typo is a compile
error, not a silent over-grant:
can: { Task: ['update'] } serializes to the wire allowlist task.update; the
server rejects any commit whose operation isn’t listed. Operations are
'read' | 'create' | 'update' | 'delete'.
Use
sessions.create({ agent }) to mint a scoped agent credential, then write
with ablo.<model>.update(...) / ablo.commits.create(...) under a claim.
This is the path for custom runtimes, MCP sessions, and protocol-level integrations.Mint
Only a secret key (sk_) can mint a session — never another session token.
The sk_ is the trust anchor; minting is your backend vouching
for the actor.
| Param | For | Meaning |
|---|---|---|
user / agent | both | The actor. id becomes the token’s participantId. Pass exactly one. |
can | agent | Per-model operation allowlist, typed off the schema. |
syncGroups | both | Narrow the session below its default scope. Omit to inherit. |
ttlSeconds | both | Lifetime in seconds. Defaults to 900 (15m). |
userMeta | both | Opaque identity blob echoed back to the client. |
Lifecycle
Sessions are short-lived by design (~15 minutes) and, for browsers, auto-refreshed — the provider re-mints ahead of expiry, so a session never drops at the boundary. A revoked or signed-out actor simply stops getting a fresh token; the old one expires on its own. There’s nothing to revoke by hand.Offline & sign-out
The short session token is not your user’s login — it’s a minutes-long credential layered on top of whatever long-lived auth yourauthEndpoint
already enforces (your own session cookie, an IdP, etc.). The provider keeps
those two lifetimes separate, which means:
- Going offline never signs the user out. The provider keeps working from
its local cache and treats a failed re-mint (no network, a timeout, a
5xxfrom your endpoint) as transient — it retries, and re-mints the instant connectivity or tab focus returns. The user stays signed in for as long as your underlying session is valid, however brief or long the network drop. - The user is signed out only when the underlying session is genuinely
rejected — i.e. your
authEndpointresponds401/403because the cookie (or IdP session) is missing, expired, or revoked. That’s the one signal the provider treats as terminal.
Your
authEndpoint contract follows from this: return the token on success,
respond 401/403 only when the user’s session is actually gone, and let
network/5xx failures surface as errors. Don’t collapse “can’t reach the mint
endpoint” into “session expired” — returning a 401 for a transient blip will
bounce a still-valid user to your sign-in page.Scope
A user session carries the user’s base sync-groups (org:/user:/team:),
derived from the identity you minted it for. Dynamic, relation-driven
membership (e.g. a dataroom:<id> the user was just added to) is resolved
server-side at connect and unioned on top — so scope stays live, not frozen
at mint time. Pass syncGroups only when you want to narrow below the default.
Security
The whole safety argument is the short TTL: a session token leaked from a browser (XSS) is valid for minutes, scoped to one actor’s data, and can’t mint anything or touch the control plane. Contrastsk_, which would be a full org
compromise — which is exactly why it never leaves your server.
User vs. agent sessions
User session (ek_) | Agent session (rk_) | |
|---|---|---|
| For | a person in the browser | an agent / automation |
| Authority | full, within their org | narrow (explicit can allowlist) |
| Mint | ablo.sessions.create({ user: { id } }) | ablo.sessions.create({ agent: { id }, can }) |
| Lives where | the user’s browser | the agent runtime |