Sign-in providers
SIWS, Google, X, Apple, and email SDK calls, the React auth UI, and React Native token storage.
Wallet message signing (SIWE) is in Authentication & sessions and Quickstart: REST. Privy sign-in is Privy Wallet Provider. This page is the other provider calls, the React auth UI, and React Native token storage.
Linking another provider onto a user who is already signed in is Account linking. Attaching your own user id is Partner External ID Linking.
Google, X, Apple, and email
These four calls use client.authStart. Google, X, and Apple return type: "redirect" and require
redirectUrl (an allowed origin). Email sends a magic link; email is required and redirectUrl
is where the link sends the user back.
Email and wallet starts can return type: "challenge_required". Resubmit the same body with
turnstileToken. Google, X, and Apple do not return that type. Widget setup is
Bot protection.
import { createAggClient, type AggAuthStartBody } from "@agg-build/sdk";
const client = createAggClient({
baseUrl: "https://api.t.sufwankhalid.com",
appId: "your-app-id",
});
// AggAuthStartBody requires `email` when provider is "email".
async function startSignIn(body: AggAuthStartBody) {
let start = await client.authStart(body);
if (start.type === "challenge_required") {
// Turnstile token for start.siteKey. See Bot protection.
start = await client.authStart({ ...body, turnstileToken: "<cf-token>" });
}
return start;
}const response = await startSignIn({
provider: "google",
redirectUrl: "https://yourapp.com/auth/callback",
});
if (response.type === "redirect") {
window.location.assign(response.url);
}sufwan-local-test then redirects to redirectUrl with a one-time code. Exchange it on your backend, or in the
browser on that page. The code is single-use.
const code = new URLSearchParams(window.location.search).get("code");
if (code) {
await client.exchangeAuthCode(code);
window.history.replaceState(null, "", window.location.pathname);
}SIWS
Solana wallet sign-in does not redirect. Ledger uses this same call: signMessage on the raw UTF-8
bytes, not Phantom's signIn() helper.
import bs58 from "bs58";
import { createAggClient } from "@agg-build/sdk";
const client = createAggClient({
baseUrl: "https://api.t.sufwankhalid.com",
appId: "your-app-id",
});
let start = await client.authStart({ provider: "siws" });
if (start.type === "challenge_required") {
// Turnstile token for start.siteKey. See Bot protection.
start = await client.authStart({ provider: "siws", turnstileToken: "<cf-token>" });
}
if (start.type !== "nonce" || !start.nonce) {
throw new Error("Unexpected auth start");
}
const message = client.buildSiwsMessage({
address: walletPublicKey.toBase58(),
chainId: "mainnet",
nonce: start.nonce,
domain: window.location.host,
uri: window.location.origin,
});
const signedBytes = await wallet.signMessage(new TextEncoder().encode(message));
const signature = bs58.encode(signedBytes);
await client.verify({ message, signature });Start authentication. Verify sign-in.
React
AggAuthProvider from @agg-build/auth completes challenge_required for the connect button.
See Bot protection and the
Connect Button reference. Wire the client in
Quickstart: React.
import { QueryClient, QueryClientProvider } from "@agg-build/hooks";
import { createAggClient } from "@agg-build/sdk";
import { AggProvider } from "@agg-build/ui";
import {
AggAuthProvider,
ConnectButton,
createEmailAuthMethod,
createGoogleAuthMethod,
} from "@agg-build/auth";
import { useSiweAuthMethod } from "@agg-build/auth/siwe";
import { WagmiProvider } from "wagmi";
import { wagmiConfig } from "./wagmi-config";
const client = createAggClient({
baseUrl: "https://api.t.sufwankhalid.com",
appId: "your-app-id",
});
const queryClient = new QueryClient();
function AuthButton() {
const siwe = useSiweAuthMethod({
statement: "Sign in to sufwan-local-test",
});
return (
<AggAuthProvider methods={[siwe, createGoogleAuthMethod(), createEmailAuthMethod()]}>
<ConnectButton />
</AggAuthProvider>
);
}
export function App() {
return (
<WagmiProvider config={wagmiConfig}>
<QueryClientProvider client={queryClient}>
<AggProvider client={client}>
<AuthButton />
</AggProvider>
</QueryClientProvider>
</WagmiProvider>
);
}createTwitterAuthMethod and createAppleAuthMethod are the same shape as createGoogleAuthMethod.
Dedicated callback pages
On a standalone callback route, useAggAuthCallback() reads the redirect:
import { useAggAuthCallback } from "@agg-build/auth";
export function AuthCallbackPage() {
const { error, isHandled, isHandling, user } = useAggAuthCallback();
if (isHandling) return <p>Finishing sign-in...</p>;
if (error) return <p>{error.message}</p>;
if (isHandled) return <p>Signed in as {user?.id}</p>;
return <p>No sufwan-local-test auth callback data was found.</p>;
}useAggAuth is the wallet hook for a custom button.
import { useAggAuth } from "@agg-build/hooks";
import { useAccount, useSignMessage } from "wagmi";
function SignIn() {
const { address, chainId } = useAccount();
const { signMessageAsync } = useSignMessage();
const { signIn, signOut, isAuthenticated, user, isLoading } = useAggAuth({
address,
chainId,
signMessage: async (message) => signMessageAsync({ message }),
});
if (isAuthenticated) {
return (
<div>
<p>Signed in as {user.id}</p>
<button onClick={signOut}>Sign Out</button>
</div>
);
}
return (
<button onClick={() => signIn("Sign in to sufwan-local-test")} disabled={isLoading}>
Sign in with Ethereum
</button>
);
}import bs58 from "bs58";
import { useWallet } from "@solana/wallet-adapter-react";
import { useAggAuth } from "@agg-build/hooks";
function SolanaSignIn() {
const { publicKey, signMessage } = useWallet();
const { signIn, isLoading } = useAggAuth({
address: publicKey?.toBase58(),
chain: "solana",
chainId: "mainnet",
signMessage: async (message) => {
const signedBytes = await signMessage!(new TextEncoder().encode(message));
return bs58.encode(signedBytes);
},
});
return (
<button onClick={() => signIn("Sign in to sufwan-local-test")} disabled={isLoading}>
Sign in with Solana
</button>
);
}Token storage
Access and refresh tokens stay in memory. The SDK writes only a non-sensitive hint to
localStorage (user id, username, avatar, wallet address, and wasAuthenticated), keyed by
appId. Restoring that hint sets authStatus to "unknown", not "authenticated".
persistSession: false skips the hint. Refresh lifetimes are in
Authentication & sessions.
| Mode | Refresh token | Survives reload |
|---|---|---|
"body" (default) | Returned by verify() and exchangeAuthCode() | No — you store it |
"cookie-refresh" | HttpOnly cookie on the sufwan-local-test API host, scoped to /auth | Yes — silent refresh on cold start |
In "cookie-refresh", those methods may omit refreshToken from the JSON.
React Native
localStorage is missing, so the hint is not stored and isAuthenticated is false after a
restart. The HttpOnly cookie is not available either. Keep "body" mode, store the refresh token
in the platform keystore, and seed it with setSession() before the first authenticated call.
import * as SecureStore from "expo-secure-store";
// session is the result of verify() or exchangeAuthCode() in "body" mode.
if (session.refreshToken) {
await SecureStore.setItemAsync("agg_refresh", session.refreshToken);
}
const refreshToken = await SecureStore.getItemAsync("agg_refresh");
if (refreshToken) {
client.setSession({ accessToken: "", refreshToken });
await client.refreshAccessToken();
}Do not put the refresh token in AsyncStorage.
Install
npm install @agg-build/sdk @agg-build/hooks @agg-build/ui @agg-build/auth
npm install wagmi
npm install @solana/wallet-adapter-react bs58Install wagmi only for Ethereum, and the Solana packages only for SIWS.