A session is not a device
A session is one live credential — the refresh half of the pair, specifically. A device is the thing holding it, and one device can hold several sessions over its life, because every rotation mints a new one.
The registry exists so a user can act on the second thing. Sign out my old phone is a sentence about a device, and answering it needs a durable id that survives every rotation in between.
A client opts in with headers
Send x-pithy-device-id at sign-in and the device is registered. A mobile client is where this earns its place. Send nothing and it is not — a device-less sign-in is a perfectly ordinary web sign-inb session, and nothing about the flow changes.
| Header | Required |
|---|---|
x-pithy-device-id | Yes. No id, no registration |
x-pithy-platform | No |
x-pithy-device-name | No |
x-pithy-device-model | No |
x-pithy-os-version | No |
x-pithy-app-version | No |
x-pithy-push-token | No |
The id is yours to generate and yours to keep stable. A value that changes per launch turns the registry into a list nobody can read.
A sparse re-login never erases what you already knew
The upsert is idempotent on (userId, deviceId). A returning device refreshes lastSeenAt and lastIp — always — and refreshes any other field only when this request actually supplied it.
That distinction is the whole design of the write. A client that sent full metadata on first sign-in and only an id on the next one would otherwise have its model, OS version and name overwritten with blanks, and the row that read iPhone 15 Pro · iOS 18.2 would become an id and nothing else. An absent header means no news, not clear it.
platform shows this most clearly: it defaults to web only on first registration. On an update, an absent platform header leaves the stored value alone, so a sparse re-login from a phone never relabels it as a browser.
createdAt is never reset. It is when this device first appeared, and that is a fact about the past.
Two routes a user’s own client calls
Both are in the reference with their shapes.
| Route | What it does |
|---|---|
GET /auth/devices | The caller’s devices, most recently seen first |
POST /auth/devices/revoke | Sign out one device, then forget it |
GET needs a valid credential and nothing more. The revoke also carries the CSRF origin guard, because it is a mutating route and a cookie session is a credential a browser attaches for you.
Revoking is two acts in one order. Every session token bound to that device is deleted through Better Auth’s own session deletion — so its bookkeeping stays consistent — and only then is the device row removed. The response is a count:
{ "revoked": 2 }Two, not one, because a device that rotated since its last cleanup can legitimately hold more than one live row.
An audit event is written. Somebody signed out a device is exactly the kind of event that matters later, when the question is when a phone stopped having access.
The admin side is a separate surface
An operator revoking somebody else’s sessions is a different act from a user revoking their own, and the kit treats it as one:
| Route | Verified by |
|---|---|
GET /auth/admin/users | control-plane |
GET /auth/admin/users/:userId | control-plane |
GET /auth/admin/devices | control-plane |
POST /auth/admin/sessions/revoke | control-plane |
POST /auth/admin/users/:userId/sessions/revoke | control-plane |
POST /auth/admin/users/:userId/devices/revoke | control-plane |
These need an issued, scoped machine credential — never a staff member’s session. A support agent who can sign a customer out because their own cookie happens to carry a role is an audit trail that cannot distinguish the tool did it from a person did it, and default-denied is the state these routes ship in.
Where a device binding shows up again
The binding is carried across rotation: a successor session inherits the device id from the one it replaces. So the registry stays accurate over a session’s whole life without the client re-sending anything, and a revoke months later still finds every row that device is holding.
A refresh-token family and a device are different groupings, and both can revoke. A family revocation is the automatic response to a detected replay. A device revocation is a person deciding. Neither is a substitute for the other.