Skip to content

Sign In With Solana

This guide shows the recommended @wallet-ui/react path for wallet-first auth:

  • connect with the stock Wallet UI picker
  • prefer native solana:signIn
  • fall back to connected-account solana:signMessage
  • verify the payload on your backend before creating a session

Start with a normal Wallet UI provider and prove wallet detection and connection before adding auth.

src/providers/solana-provider.tsx
import { createSolanaDevnet, createWalletUiConfig, WalletUi } from '@wallet-ui/react';
import type { ReactNode } from 'react';
const config = createWalletUiConfig({
clusters: [createSolanaDevnet()],
});
export function SolanaProvider({ children }: { children: ReactNode }) {
return <WalletUi config={config}>{children}</WalletUi>;
}
src/components/wallet-connect.tsx
import { WalletUiDropdown } from '@wallet-ui/react';
export function WalletConnect() {
return <WalletUiDropdown />;
}

Use useWalletUiAuth({ wallet }) for headless auth UI. It prefers native solana:signIn for the selected wallet and falls back to solana:signMessage for the connected account.

src/components/sign-in-button.tsx
import { useWalletUi, useWalletUiAuth } from '@wallet-ui/react';
export function SignInButton() {
const { wallet } = useWalletUi();
if (!wallet) {
return <button disabled>Connect wallet</button>;
}
return <ConnectedSignInButton key={wallet.name} wallet={wallet} />;
}
function ConnectedSignInButton({ wallet }) {
const { canSignIn, isSigningIn, reason, signIn } = useWalletUiAuth({ wallet });
async function handleSignIn() {
try {
const result = await signIn({
input: {
domain: window.location.host,
nonce: await fetch('/api/auth/siws/nonce').then(res => res.text()),
statement: 'Sign in to Example App.',
uri: window.location.origin,
version: '1',
},
});
const response = await fetch('/api/auth/siws/verify', {
body: JSON.stringify({
account: {
address: result.account.address,
publicKey: Array.from(result.account.publicKey),
},
input: result.input,
method: result.method,
signature: Array.from(result.signature),
signedMessage: Array.from(result.signedMessage),
}),
headers: { 'content-type': 'application/json' },
method: 'POST',
});
if (!response.ok) {
throw new Error(`SIWS verification failed (${response.status})`);
}
} catch (error) {
console.error('SIWS sign-in failed', error);
}
}
return (
<button disabled={!canSignIn || isSigningIn} onClick={handleSignIn}>
{isSigningIn ? 'Signing in...' : reason ? 'Connect wallet' : 'Sign in'}
</button>
);
}

Native SIWS uses the Wallet Standard solana:signIn feature. The wallet constructs and signs the SIWS message, then returns the account, signed message bytes, and signature.

Fallback SIWS uses the connected account’s solana:signMessage feature. useWalletUiAuth constructs a SIWS-compatible message from the input, fills address from the connected account, fills domain from input.domain or location.host, signs the bytes, and returns the same backend payload shape.

For custom flows, @wallet-ui/react also re-exports lower-level hooks from @solana/react. Use useSignIn(wallet) when you need direct native SIWS control, and use account-level message signing such as useSignMessage(account) or useWalletAccountMessageSigner(account) when you need custom fallback behavior.

Send enough data for the backend to verify the exact bytes that were signed.

FieldPurpose
account.addressAddress that requested the session.
account.publicKeyPublic key bytes used for Ed25519 verification.
inputSIWS challenge fields the server issued or accepted.
methodEither solana:signIn or solana:signMessage.
signatureSignature bytes returned by the wallet.
signedMessageExact message bytes returned by the wallet.

Server-side verification should:

  • verify the Ed25519 signature against signedMessage and account.publicKey
  • parse signedMessage as SIWS and compare expected domain, address, nonce, uri, version, and time fields
  • reject used, unknown, expired, or wrong-domain nonces
  • reject mismatches between the parsed address and the authenticated account
  • create a session only after verification succeeds
LayerUse it for
Raw wallet.featuresDiagnostics and advanced feature gating.
Wallet-level hooksNative wallet actions such as useSignIn(wallet).
Account-level hooksConnected-account fallback signing such as useSignMessage(account).
useWalletUiAuthHeadless app auth flows that choose native SIWS first and fallback signing second.
WalletUiAuthWallet-list auth buttons that should connect before signing.

For connected-wallet auth fallbacks, prefer the account-level signing path over manual raw feature probing unless you truly need low-level control.

Runtime Wallet Standard features are the source of truth. The public-docs column was checked on April 30, 2026, and should be treated as guidance only; verify the exact wallet version, browser or device, wallet.features, and account.features in your app.

WalletPublic docs checkedNative solana:signInsignMessage fallbackRuntime verification target
BackpackDeep-link signMessageNot confirmed from public docsDocumentedConfirm standard:connect on the wallet and solana:signMessage on the connected account.
JupiterJupiter Mobile user docsNot confirmed from public docsNot confirmed from public docsTest the target Jupiter surface directly; public docs confirm a Solana-native wallet, not browser auth feature flags.
PhantomSIWS and message signingDocumentedDocumentedConfirm extension or platform version and that wallet.features exposes solana:signIn.
SolflareDeep-link signMessageNot confirmed from public docsDocumentedConfirm standard:connect on the wallet and solana:signMessage on the connected account.

For every wallet you support, record:

  • wallet name, wallet version, browser or device, and checked date
  • wallet.features and the selected account.features
  • whether WalletUiAuth chose native solana:signIn or message fallback
  • backend verification result for the returned signedMessage and signature

The Vite React example includes an /auth route that prints the verification payload shape for manual wallet checks.

StateLikely causeNext check
auth-unsupportedThe wallet has neither native SIWS nor account message signing.Inspect wallet.features and account.features.
missing-domainFallback signing could not build a SIWS message.Pass input.domain from the frontend or backend challenge.
wallet-not-connectedFallback signing tried to connect the wallet but received no account.Connect the target wallet and inspect returned accounts.
  1. Prove the stock picker and connect flow with WalletUiDropdown.
  2. Add useWalletUiAuth and verify native solana:signIn with a wallet that supports it.
  3. Verify solana:signMessage fallback with a connected account.
  4. Add backend nonce issuance, verification, and session creation.
  5. Customize the wallet and auth UI after the default path works.

References: Phantom SIWS announcement, Phantom SIW docs, Phantom signMessage docs, Backpack signMessage docs, Jupiter Mobile docs, and Solflare signMessage docs.