better-auth-cross-device
pnpm add @onmax/better-auth-cross-device
import { crossDevice } from "@onmax/better-auth-cross-device"
export const auth = betterAuth({
plugins: [
crossDevice({
appName: "Console",
trustedOrigins: ["https://console.example.com"],
adapters: [passkeyAdapter],
resolveLogin: async ({ approvedUserId }) => {
if (!approvedUserId) return null
return users.findById(approvedUserId)
},
}),
],
})
The plugin owns the short-lived order, device-role tokens, endpoint wiring, and lifecycle checks. Your adapter owns the proof format.
What you get
icon: i-lucide-server title: Server plugin
Adds Better Auth endpoints, a dedicated order model, lifecycle validation, and session finalization for approved login orders.
icon: i-lucide-monitor-smartphone title: Client helpers
Starts orders, opens claim URLs, subscribes to events, and finalizes the approved result from the initiating device.
icon: i-lucide-key-round title: Proof adapters
Lets you verify passkeys, wallet signatures, or app-specific approvals without changing the order lifecycle.
icon: i-lucide-radio title: Live status
Uses server-sent events so the initiating device can react when the approving device claims, approves, rejects, or expires the order.
How it works
| Step | Device | What happens |
|---|---|---|
| 1 | Initiating device | Starts an order and shows the claim URL as a link or QR code. |
| 2 | Approving device | Claims the order and receives a challenge token. |
| 3 | Approving device | Reads the challenge and submits adapter-specific proof. |
| 4 | Server | Verifies the proof through your adapter and marks the order approved. |
| 5 | Initiating device | Receives the event and finalizes the result. |
120 seconds, and each device role gets a separate token.