Cavos

React

Drop-in React bindings for @cavos/kit — provider, hooks, and the customizable login modal.

@cavos/kit ships React bindings under the @cavos/kit/react subpath (same package). Wrap your app once in <CavosProvider> and every descendant can call useCavos() to read wallet state and trigger actions — no prop drilling, no imperative connect calls.

Terminal
npm install @cavos/kit
app/provider.tsx
import { CavosProvider } from "@cavos/kit/react";

export function Providers({ children }) {
  return (
    <CavosProvider
      config={{
        appId: process.env.NEXT_PUBLIC_CAVOS_APP_ID,
        chains: ["solana", "stellar"],
        defaultChain: "solana",
        network: "testnet",
        appSalt: "my-app",
        socialRecovery: true,   // optional — see Recovery
      }}
      modal={{
        appName: "My App",
        theme: "light",
      }}
    >
      {children}
    </CavosProvider>
  );
}

The provider manages one or more chains. Behind the scenes it resolves the identity, derives addresses for all configured chains, and keeps the wallet handles ready for sponsored transactions. Deployment happens lazily on first execute per chain — see Concepts.

Props

PropDescription
configChains, network, appSalt and credentials. One or more chains via chains + defaultChain.
modalLogin modal appearance and providers. Omit to render no modal.
identityBring your own auth. Pass your signed-in user and Cavos login turns off. null while your auth loads or after sign-out. See Authentication.

In development the provider checks the config when it mounts and reports problems in the console — a missing appSalt, a paymaster key on a chain that ignores it, and in particular an appSalt that changed since this browser last connected, which silently moves every existing user to a new, empty wallet. Run the same checks yourself with validateCavosConfig(config).

useCavos()

The primary hook. Returns the full wallet surface:

import { useCavos } from "@cavos/kit/react";

function Wallet() {
  const { address, isAuthenticated, walletStatus, openModal, logout } = useCavos();
  if (!isAuthenticated) return <button onClick={openModal}>Sign in</button>;
  return <div>{address} · ready={walletStatus.isReady}</div>;
}

State

FieldDescription
isAuthenticated: booleanWhether a wallet is connected.
user: UserInfo | null{ userId, email?, provider? } from the login.
wallet: CavosWallet | nullThe connected wallet for the selected chain, discriminated by wallet.chain. Narrow on it before chain-native calls.
session: CavosWallet & CavosSession | nullThe full multi-chain session. Use session.wallet("stellar") to access other chains.
address: string | nullThe account address for the selected chain.
chain: ChainThe active chain ("starknet" | "solana" | "stellar").
configuredChains: Chain[]All chains configured for this session.
walletStatus: WalletStatusDeployment / device-approval / passkey flags (see below).
isLoading: booleanTrue during an in-flight login or connect.
authError: string | nullLast unrecoverable auth/connect error. Null while healthy.
passkeySupported: booleanWhether this device/browser can use a platform passkey.

WalletStatus: { isDeploying, isReady, isUndeployed, needsDeviceApproval, awaitingApproval, pendingRequestId, hasPasskey, isNewAccount, isSocialRecovering, socialRecoveryReadyAt, recovery }.

  • isUndeployed: True when the account has not been deployed on-chain yet. First execute will deploy + run atomically.
  • isReady: True when deployed and this device is authorized.
  • needsDeviceApproval: True when deployed but this device is not authorized.

Both isUndeployed and isReady allow execution. Only needsDeviceApproval blocks transactions. An undeployed wallet can sign messages and execute — the first execute deploys the account.

walletStatus.recovery is the one flag most apps actually read: { protected: boolean; methods: ("passkey" | "social")[] } — can this user get back in from a device they do not have yet? The other flags describe work in flight; this describes the standing guarantee.

const { walletStatus } = useCavos();
if (walletStatus.isReady && !walletStatus.recovery.protected) {
  // Nudge: "Add a passkey so you can sign in on a new device."
}

recovery.protected starts false and flips once the lookup that fills it returns. Read it as "not known to be protected" during the first moments of a connect, not as "known to be unprotected" — don't show a scary warning on frame one.

Chain switching

Switch the selected chain without re-authenticating or remounting:

const { chain, setChain, configuredChains, session } = useCavos();

// Switch to Stellar
await setChain("stellar");

// Now `chain` is "stellar", `wallet` is the Stellar wallet
// The walletStatus updates to reflect the Stellar account's state

setChain does not re-deploy or re-authenticate — it just changes which wallet is active. The session keeps all chains connected.

Login

MethodDescription
login(provider)OAuth social login ("google" | "apple") — opens the hosted flow.
sendOtp(email)Send an email OTP code.
verifyOtp(email, code)Verify an OTP / complete a magic link and deploy the wallet.
sendMagicLink(email)Send a passwordless magic-link email.
handleCallback(authData, redirectUri?)Resolve identity from an OAuth callback (?cavos_auth_code=…) and deploy. Pass the callback URL as redirectUri if it differs from the current one.

Transactions

MethodDescription
execute(calls, opts?)Starknet only. Multicall signed by the device key, gasless by default. Returns { transactionHash }. Works on undeployed (deploys first) and ready wallets.
signMessage(message)All chains. Sign an arbitrary message off-chain; returns a uniform MessageSignature. Works on undeployed and ready wallets. See Wallet actions.

On Solana and Stellar, call the wallet directly for chain-native actions instead of the context's execute: narrow on wallet.chain, then wallet.execute(amount, destination). For signTransaction (sign without submit), also call wallet.signTransaction(...) directly after narrowing — the args differ per chain. See Wallet actions.

Device & recovery

MethodDescription
addSigner(pubkey)Authorize another device signer (gasless add_signer).
removeSigner(pubkey)Revoke a device signer (gasless remove_signer) — the "this wasn't me" escape hatch. Must run from a device that is already authorized, and cannot revoke itself. On Stellar use wallet.removeDevice(...) instead: devices there are envelope slots, not signer pubkeys.
listDevices()Device signer pubkeys currently authorized on this wallet, for a management UI. Starknet/Solana only — on Stellar call wallet.listDevices() for the envelope slot ids.
enrollPasskey(passkey, params)Low-level passkey enrollment when you drive WebAuthn yourself. Most apps want enrollPasskeyDefault().
enrollPasskeyDefault()Enroll a synced passkey as an approver (2FA for new devices). Works on undeployed (pending until first execute) and ready wallets.
approveDeviceWithPasskey()From a needs-device-approval browser, prompt the synced passkey to authorize this device.
setupRecovery()Register a backup signer derived from a generated recovery code. Resolves with the code — show it to the user once. Works on undeployed (pending until first execute) and ready wallets.
submitSocialRecoveryToken(idToken)Drive social recovery with a provider id_token your own login obtained. Enrols when the device is ready, recovers when it is not. See Hardware-isolated recovery.
recover(code)Recover access after losing every device. Brings the provider to a ready state.
resendDeviceApproval()Re-request the device-approval email for the current pending request.

When hardware-isolated social recovery is enabled in the selected environment, the dashboard's single provider overrides modal.providers. Email is forced to magic-link mode; OTP is not used for this recovery flow. The provider automatically enrolls a ready wallet after fresh authentication and restores an unregistered device through the attested enclave.

These wrappers present a shared product flow across the current adapters. Their cryptographic implementation and some availability constraints remain chain-specific. See Passkeys and Recovery for the capability details.

MethodDescription
openModal() / closeModal()Open/close the built-in auth modal (the provider mounts it automatically when modal is set).
logout()Sign out and clear local wallet state.
clearAuthError()Clear authError (e.g. when the user starts a new login attempt).

useCavosAuth()

A deliberately thin subset for components that only need to open the modal and read basic auth state — no wallet, no transactions:

{
  openModal, closeModal,
  isAuthenticated, address, user, walletStatus, logout,
}

<CavosAuthModal>

A fully customizable, themeable login modal. The provider mounts it automatically when you pass modal={...}; you can also render it directly (for a live preview, or to control its lifecycle yourself).

import { CavosAuthModal } from "@cavos/kit/react";

<CavosAuthModal
  open={open}
  onClose={() => setOpen(false)}
  appName="My App"
  appLogo="https://myapp.com/logo.png"
  appLogoSize={56}
  providers={["email", "google", "apple"]}
  emailMode="otp"
  primaryColor="#402AFF"
  theme="light"
  radius={16}
  secureStep="optional"
/>

Props

PropTypeDescription
openbooleanWhether the modal is shown (ignored when inline).
onClose() => voidCalled when the user dismisses the modal.
appNamestring?Shown in the heading ("Sign in to {appName}").
appLogostring?Image URL for the logo. If omitted, the Cavos star is shown. The provider also loads it from your app's dashboard config automatically; a local appLogo overrides that.
appLogoSizenumber?Logo height in px. Defaults to 40 (image) / 34 (Cavos star).
providers("google" | "apple" | "email")[]?Which login buttons to show. Defaults to all three.
emailMode"magic-link" | "otp"?How the email provider authenticates. Defaults to magic-link.
primaryColorstring?Accent color for buttons / focus rings (hex).
theme"light" | "dark"?Card theme. Defaults to light.
backgroundColorstring?Override the card background (defaults to white / #111 per theme).
radiusnumber?Card & button corner radius in px (card defaults to 16).
inlineboolean?Render the card in-flow (no overlay/backdrop) — for live previews. When true, open is ignored.
secureStep"optional" | "required" | "off"?The one-time "secure your account" step after a new account. optional (default) shows Skip; required forces it; off skips it.
onSuccess(address: string) => void?Fired when authentication + deployment succeed.

CavosModalConfig

The object you pass as modal={...} to <CavosProvider>. Same fields as CavosAuthModalProps except no open / onClose / inline (the provider manages those), plus onSuccess.

Theming

All visual props (primaryColor, theme, backgroundColor, radius, appLogo, appLogoSize) apply identically whether you set them on <CavosProvider modal={...}> or on a direct <CavosAuthModal ... />.

<CavosProvider
  config={{ chains: ["solana"], defaultChain: "solana", network: "testnet", appSalt: "my-app", appId }}
  modal={{
    appName: "My App",
    appLogo: "/logo.png",
    appLogoSize: 48,
    primaryColor: "#7C3AED",
    theme: "dark",
    backgroundColor: "#0A0A0F",
    radius: 20,
  }}
>
  <App />
</CavosProvider>

Mobile: automatic bottom sheet

On viewports max-width: 640px the modal becomes a bottom sheet — it slides up from the bottom with rounded top corners and a grab handle, overlaying the page with a blurred backdrop. This is fully automatic; there is no prop to toggle. On desktop the modal renders centered. The inline mode disables the overlay/sheet entirely (the card is embedded in your layout).

Device approval & revocation pages

Every integrating app needs two routes: one where a user approves a new device, and one where they revoke a device they don't recognise (the links in the device-added email). Both ship ready to mount:

app/approve-device/page.tsx
import { ApproveDevicePage } from "@cavos/kit/react";

const configs = [
  { chains: ["starknet"], defaultChain: "starknet", network: "testnet", appSalt: "my-app", appId, paymasterApiKey },
  { chains: ["solana"], defaultChain: "solana", network: "testnet", appSalt: "my-app", appId },
];

export default function Page() {
  return <ApproveDevicePage configs={configs} />;
}

Pass every chain your app supports — the request carries its own network and the page selects the matching config (configForNetwork / chainForNetwork do this, and are exported if you mount the provider yourself). Mounting the wrong chain's provider fails in a way that reads like the device is gone.

RevokeDevicePage is the same flow with the opposite verb. Pass a function as children to keep your own markup — see Multi-device.

Multi-chain in React

The provider handles multiple chains natively. Configure them all up front:

<CavosProvider
  config={{
    chains: ["solana", "stellar", "starknet"],
    defaultChain: "solana",
    network: "testnet",
    appSalt: "my-app",
    appId,
    paymasterApiKey, // for Starknet
  }}
  modal={{ appName: "My App" }}
>
  <App />
</CavosProvider>

Then switch chains without remounting:

function ChainSwitcher() {
  const { chain, setChain, configuredChains, walletStatus } = useCavos();

  return (
    <select value={chain} onChange={(e) => setChain(e.target.value as Chain)}>
      {configuredChains.map((c) => (
        <option key={c} value={c}>{c}</option>
      ))}
    </select>
  );
}

setChain updates the active chain, and walletStatus reflects the new chain's status. No re-authentication, no remount, no lost state.

Each chain has its own address, deployment status, and device authorization. Switching chains may show a different walletStatus — one chain might be ready while another is undeployed or needs-device-approval.

Chain and NetworkEnv types are exported from the core entry (import type { Chain } from "@cavos/kit"), not from @cavos/kit/react.

On this page