Sign in with a wallet
Coordinate a nonce, wallet signature, and backend verification with createSignInFlow.
createSignInFlow from @usebutr/core coordinates signing with an already
connected wallet. Your application owns nonce issuance, signature verification,
and sessions. butr defines no wire format: use the authentication format your
backend understands, such as SIWE or SIWS.
Wire the flow
Provide a single-use nonce fetcher and a verifier. This factory keeps those application-specific operations explicit:
import { createSignInFlow, type SignInFlowOptions } from "@usebutr/core";
export const makeSignIn = (
getNonce: SignInFlowOptions["getNonce"],
verify: SignInFlowOptions["verify"],
buildMessage: NonNullable<SignInFlowOptions["buildMessage"]>,
) => createSignInFlow({ buildMessage, getNonce, verify });
// const flow = makeSignIn(fetchNonce, verifyOnServer, buildSiweMessage);
// await flow.signIn(connectedWallet);The getNonce callback receives { account, wallet }. buildMessage receives
{ account, nonce, wallet }. verify receives the signed result and must reject
if verification fails. Callback failures and wallet rejections propagate to the
caller; signIn() resolves only after verify() succeeds.
What runs, and when
- Check
wallet.connector.capabilities.signMessage. If false, throwSignInUnsupportedErrorbefore fetching a nonce or touching the wallet. - Fetch a nonce for the explicit account argument, or
wallet.account. - For an SVM wallet advertising
signInand providingconnector.signIn, use the wallet's SIWS path unlesspreferSignMessage: truewas passed. - Otherwise build the message and call
signMessagewith its UTF-8 bytes and the selected account. - Add base64 mirrors of the signature and signed bytes, then call
verify.
The SIWS path passes { nonce } to the wallet. The wallet composes the statement,
so result.message is absent and result.account comes from the wallet's output.
buildMessage is unused on that path. With preferSignMessage: true, every
platform uses your message builder instead.
buildMessage is optional in the API. Its default is a simple address-and-nonce
message; it is not a complete SIWE or SIWS authentication statement. Supply a
builder matching your backend's format when using the message-signing path.
Verify the bytes the wallet signed
SignInResult includes account, wallet, nonce, optional message,
signature, signedMessage, signatureBase64, and signedMessageBase64.
Verify against signedMessageBase64, decoded to bytes on the server, with
signatureBase64. Wallets may wrap or re-encode the original message. Comparing
only message can verify different bytes from those actually signed.
Send the fields your backend expects rather than serializing the whole result:
wallet contains connector methods, and raw Uint8Array fields do not retain
their byte-array shape through JSON. Base64 mirrors are provided for this boundary.
The backend must validate the expected identity, nonce, domain, and expiry for
its chosen authentication format before establishing a session.
Unsupported wallets
Catch SignInUnsupportedError separately from user rejection. Its connectorId
identifies the wallet that cannot sign in; no signing prompt or nonce request
has occurred. Offer a signing-capable wallet in that case.
Source: packages/core/src/sign-in/sign-in-flow.ts and
packages/core/src/__tests__/sign-in-flow.test.ts. No demo currently uses this flow. See
signing, capabilities, and the core API.