Server Setup
Register crossDevice() in your Better Auth plugins list. The plugin creates the order schema and exposes the cross-device endpoints under /cross-device by default.
Quick Start
1. Create a proof adapter
Adapters translate a canonical challenge into the proof format your approving device understands. They also verify the submitted proof.
import { betterAuth } from "better-auth"
import {
crossDevice,
CrossDeviceProofError,
type CrossDeviceAdapter,
} from "@onmax/better-auth-cross-device"
const passkeyAdapter: CrossDeviceAdapter = {
id: "passkey",
createChallenge: (challenge) => JSON.stringify(challenge),
async verifyProof({ challenge, proof }) {
const verified = await verifyPasskeyProof(challenge, proof)
if (!verified.ok) {
throw new CrossDeviceProofError("Passkey proof failed", {
code: "UNAUTHORIZED",
})
}
return {
ok: true,
subject: verified.credentialId,
userId: verified.userId,
identity: verified.identity,
}
},
}
2. Register the plugin
Pass the adapter to crossDevice(). For login orders, add resolveLogin so an approved proof can become a Better Auth user.
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)
},
}),
],
})
trustedOrigins scoped to the app origins that are allowed to start orders. Origin enforcement is enabled by default.Adapter contract
Adapters own the proof format. The plugin owns order state, token checks, challenge construction, and endpoint wiring.
| Hook | Purpose |
|---|---|
createChallenge | Convert the canonical challenge envelope into the adapter message. |
verifyProof | Validate the submitted proof and return the approved subject. |
createChallenge receives the challenge envelope without message plus appName. Return the exact string the approving device should sign, display, or verify.
verifyProof receives the full challenge envelope and the submitted proof. Return ok: true with the verified subject when the proof is valid.
const adapter: CrossDeviceAdapter = {
id: "wallet",
createChallenge: ({ appName, origin, nonce, exp }) => {
return `${appName} approval for ${origin}\nnonce=${nonce}\nexp=${exp}`
},
verifyProof: async ({ challenge, proof }) => {
const result = await verifyWalletSignature(challenge.message, proof)
return {
ok: true,
subject: result.address,
identity: { address: result.address, chainId: result.chainId },
}
},
}
Login resolution
For login orders, resolveLogin maps a verified proof to a Better Auth user. Return null when the proof is valid but should not create a session.
resolveLogin: async ({ approvedSubject, approvedIdentity }) => {
return users.findByExternalSubject(approvedSubject, approvedIdentity)
}
For sign and transaction orders, finalization returns the approved proofArtifact instead of creating a session.
Configuration
| Option | Use it when |
|---|---|
appName | You want challenges to name the product or console being approved. |
endpointPrefix | Your Better Auth API needs a different endpoint namespace. |
trustedOrigins | Only specific app origins may start orders. |
orderTtlSeconds | Orders need a shorter or longer approval window. |
claimPathTemplate | Your approving-device route is not /cross-device/claim/{orderId}?token={claimToken}. |
enforceOrigin | Keep enabled unless the adapter verifies origin inside the proof. |
Next Steps
- Client setup: start and follow an order from the browser
- Cross-device flow: understand states, events, and tokens
- Security: review origin checks and proof failure behavior