Cavos

Authentication

Use Cavos hosted login (Google / Apple / email code) 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 on Starknet, Solana, and Stellar (MasterDEK wrap); see Hardware-isolated recovery. Grandfathered Stellar wallets still need a passkey or recovery code.

Option B — Cavos hosted auth (CavosAuth)

CavosAuth provides Google, Apple, and email-code 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,
});

Each sign-in URL carries the hash of a one-time secret the SDK keeps in sessionStorage, and handleCallback rejects a response that does not match it. Build the URL right before redirecting, and handle the callback in the same tab and origin that started the login.

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 code

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,
});

The code is 9 characters (lowercase letters and digits). The Cavos recovery enclave, running in AWS Nitro, generates it, mails it directly to the user's mail server, and verifies it. The user enters the code in your app, and the SDK sends it to the enclave encrypted to its attested key. The enclave answers with its own signed token (issuer https://enclave.cavos.xyz, keys at https://enclave.cavos.xyz/.well-known/jwks.json), bound to a login secret the SDK keeps, the same binding Google and Apple tokens have. Email users therefore get hardware-isolated recovery like Google and Apple users. The user id of an email login is derived from the address.

  • Lifetime. A code expires after 5 minutes and allows 3 attempts. A new code for the same address can be requested after 60 seconds.
  • Sender. The mail comes from no-reply@cavos.xyz, shows your app's name as the sender, and has the subject Your {App} sign-in code. Its design is fixed; per-app email templates do not apply to sign-in codes.
  • Supported addresses. Providers whose domain publishes an MTA-STS policy, for example Gmail, Outlook / Hotmail, and Proton Mail (Yahoo publishes one in testing mode). Other domains get This email provider is not supported yet. (error code email_undeliverable).
  • React Native. Email sign-in is not available yet: sendOtp and verifyOtp throw an unsupported error there. Use Google or Apple.

CavosAuthModal shows a 9-character code screen and accepts a pasted code with spaces or hyphens. For the errors the code flow can return, see Troubleshooting.

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; email sign-in is not available on React Native yet.

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. On native Solana and Stellar it is also the HKDF salt for the spend key, so changing it derives a different address that will not match the registry row. Never change it. The registry key remains (userId, appId, chain, network).

On this page