Authentication
Use Cavos hosted login (Google / Apple / OTP) or bring your own identity.
Authentication exists for one reason: to produce a stable userId (plus
optional email) that the Cavos registry keys on. The login never signs
transactions — that is always the device key.
You have two options: bring your own identity, or use Cavos hosted auth.
Option A — Bring your own identity
If you already authenticate users (your own auth, Clerk, Auth0, etc.), just pass
a stable userId:
const cavos = await Cavos.connect({
chain: "solana",
network: "testnet",
appSalt: "my-app",
identity: { userId: user.id, email: user.email },
appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
});userId must be stable and unique per user. If it changes, the registry
lookup is for a different user — resulting in a different (or new) wallet. Use
an immutable primary key, not an email that can change.
In React
Pass the same identity to <CavosProvider> and you get the full React surface —
wallet status, device approval, passkeys, recovery — driven by your own auth.
Cavos login turns off: the modal is not mounted and login() throws.
export function Providers({ children }) {
const { user, isLoading } = useMyAuth(); // Clerk, Auth0, your own
return (
<CavosProvider
config={config}
identity={isLoading || !user ? null : { userId: user.id, email: user.email }}
>
{children}
</CavosProvider>
);
}Pass null while your auth is loading and once the user signs out. The provider
follows you: it connects when an identity appears, clears its state when it goes
away, and swaps wallets when the user changes.
The identity is never persisted. Your auth is the source of truth on every mount, so a Cavos session cannot outlive the session that authorized it — unlike Cavos hosted auth, which remembers its own logins so a reload can reconnect silently.
Social recovery also works from your own auth; see Hardware-isolated recovery.
Option B — Cavos hosted auth (CavosAuth)
CavosAuth provides Google, Apple, and email OTP / magic-link login that resolves
to a Cavos Identity you pass straight into connect.
import { CavosAuth } from "@cavos/kit";
const auth = new CavosAuth({
appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
// backendUrl defaults to the Cavos hosted services
});OAuth (Google / Apple)
OAuth is multi-step (redirect out, handle the callback), so resolve the identity
first, then call connect with it.
// 1. Redirect the user to the provider
const url = await auth.getGoogleOAuthUrl(/* redirectUri? */);
window.location.href = url; // or auth.getAppleOAuthUrl()
// 2. On your callback route, exchange the result for an identity.
// The callback carries a short-lived one-time code (`?cavos_auth_code=…`)
// that is redeemed server-side — identity material never sits in the URL.
const identity = await auth.handleCallback(window.location.search);
// 3. Connect with the resolved identity
const cavos = await Cavos.connect({
chain: "solana",
network: "testnet",
appSalt: "my-app",
identity,
appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
});Register every callback URL for your app in Dashboard → App → Callback URLs,
one per line, matched exactly — scheme, host, path, no trailing slash, and no
wildcards. OAuth rejects a redirect_uri that is not registered, including when
the app has none registered at all. This is deliberate: the callback carries a
one-time code, and honouring an unregistered URI would hand that code to whoever
asked for it. Remember to add tunnel or preview domains while developing, since
each one is a distinct origin.
Email OTP
await auth.sendOtp(email);
const identity = await auth.verifyOtp(email, code);
const cavos = await Cavos.connect({
chain: "solana",
network: "testnet",
appSalt: "my-app",
identity,
appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
});sendMagicLink(email) is also available as an alternative to OTP.
React Native authentication
React Native apps use NativeCavosAuth from @cavos/kit/react-native. It opens
Google or Apple in the system browser and returns through the registered
redirectUri; OTP and magic links use the same app ID and callback validation.
import { NativeCavosAuth } from "@cavos/kit/react-native";
const auth = new NativeCavosAuth({
appId,
redirectUri: "myapp://auth",
backendUrl: "https://cavos.xyz",
});
const identity = await auth.login("google");
const wallet = await Cavos.connect({
chain: "stellar",
network: "testnet",
appSalt: "my-app",
appId,
identity,
});The native auth layer persists only the public Identity and pending nonce in
application-private storage. It never stores device private keys or OAuth
tokens. Register every mobile scheme or universal link exactly in the Cavos
dashboard; unregistered redirects are rejected by the backend.
How identity becomes an address
When you call connect, the kit looks up the address in the Cavos registry:
(userId, appId, chain) → registry lookup → addressIf the registry has an entry, that address is returned. If not, this device
computes a candidate address and claims it. The registry is insert-only: the
first device to register wins. A second device connecting later sees the
existing address and enters needs-device-approval instead of creating a
duplicate wallet.
The appSalt parameter names the local device-key slot — it keeps device keys
separate across different apps on the same device. It does not determine the
address. The registry maps this user and app to the address; appSalt isolates
the device key.