Cavos

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:

TypeScript
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.

app/providers.tsx
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.

TypeScript
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.

TypeScript
// 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

TypeScript
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 → address

If 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.

On this page