Device authorization
Device authorization signs a user in on a device or app that can’t host your sign-in form, such as a CLI, a desktop app or a Figma plugin. The device asks your backend for a code and opens a URL. The user approves the code in the browser where they’re signed in to your app, and the device receives a session. It runs on Better Auth’s device authorization plugin.
Turn it on
Set the flag:
Set two variables on each Convex deployment:
DEVICE_AUTHORIZATION_CLIENT_IDis any string you choose. Your device sends the same value with every call; any other value fails withInvalid client_id. One deployment accepts one client id.BETTER_AUTH_FALLBACK_URLis your app’s URL, and it’s required here. The approval URL is built from it, and without itrequestDeviceCodefails withBETTER_AUTH_FALLBACK_URL must be set when device authorization is enabled.The setup guide sets it to your local URL; set your production URL on the production deployment.
The device-authorization/convex and device-authorization/routes registry items install the backend functions and the approval page. jsrepo add --all includes both.
The approval page
The page reads the code, checks it with Better Auth, and offers Approve and Deny. It requires a session: a signed-out user is sent to /signin?redirectTo=… and comes back to the code after signing in.
| URL | /device?user_code=<code> |
| File | src/routes/(auth)/device/+page.sveltesrc/app/device/page.tsx, which renders <DeviceAuthorization code={code} /> |
The URL the device receives, verification_uri_complete, is <BETTER_AUTH_FALLBACK_URL>/device?user_code=<code>, which opens this page in both frameworks.
A code is valid for 7 days (expiresIn: '7d' in src/convex/auth.ts). The request carries the scope your device client passes to requestDeviceCode, if any. Better Auth stores it and returns it with the token; the Auth UI doesn’t check scopes anywhere.
The flow on the device
The device calls three public Convex functions in src/convex/deviceAuthorization.ts. Each takes the clientId.
| Function | Type | Returns |
|---|---|---|
requestDeviceCode({ clientId, scope? }) | mutation | device_code, user_code, verification_uri, verification_uri_complete, expires_in and interval. |
getDeviceCodeStatus({ deviceCode, clientId }) | query | The stored status, 'pending', 'approved' or 'denied', or 'unknown' when no code matches, for example after it has been exchanged. An expired code that is still stored keeps its status; createDeviceToken refuses it with expired_token. |
createDeviceToken({ deviceCode, clientId }) | mutation | After approval: access_token (a Better Auth session token), token_type, expires_in and scope. The code can be exchanged once. |
- Call
requestDeviceCode, show theuser_code, and openverification_uri_completein the user’s browser. - Subscribe to
getDeviceCodeStatuswith thedevice_code. A Convex query updates by itself, so there’s nothing to poll. - When the status is
approved, callcreateDeviceTokenonce and keep theaccess_token. - Exchange the access token for a Convex token whenever the Convex client asks for one.
Example: a Svelte client
This client could be a Figma plugin UI or a desktop app built with Svelte. It imports the generated api of the same Convex project. Any Convex client follows the same steps.
The root component sets up Convex and hands it the session. VITE_SITE_URL is your web app’s URL, where /api/auth runs, and VITE_CONVEX_URL is the Convex deployment URL:
setupAuth re-reads the getter when externalSession.accessToken changes, so the Convex client signs in as soon as the exchange finishes. Store the access token if the device should stay signed in across restarts.
Related
- Configure features: every flag and environment variable.
- Better Auth’s device authorization plugin: the protocol and the plugin’s options.