Configure Authentication
Anonymous Authentication
Let users interact with your app without an account, then upgrade to a full account while preserving their data.
Anonymous authentication lets users start using your app immediately -- no email, no password, no sign-up form. Banata Auth creates a temporary user record and session behind the scenes, so your users can explore features, save preferences, and create content right away. When they are ready, they upgrade to a full account and everything they did as a guest carries over seamlessly.
Use Cases
Anonymous auth shines wherever you want to reduce friction and let people experience your product before committing:
- Try-before-signup -- Let users explore features, create content, or configure settings before asking them to register.
- Guest checkout -- Accept purchases or form submissions without requiring an account upfront.
- Onboarding flows -- Capture preferences or progress during onboarding, then prompt for account creation at a natural breakpoint.
- Collaborative tools -- Let guests join a shared session or document via a link, then optionally claim their identity later.
- Mobile-first experiences -- Reduce drop-off by deferring account creation until the user sees value.
Enable Anonymous Auth
Via the Dashboard
- Go to Authentication > Methods in your project.
- Toggle on Anonymous.
- Make sure at least one upgrade method is also enabled (Email & Password, Social OAuth, Magic Links, etc.) so anonymous users have a path to a full account.
Via the SDK
You can also enable anonymous auth programmatically:
await banata.configuration.saveDashboardConfig({
authMethods: {
anonymous: true,
emailPassword: true, // at least one upgrade method
},
});Note: Anonymous auth is most useful when combined with at least one other authentication method that users can upgrade to. Without an upgrade path, anonymous users have no way to secure their account.
Client-Side API
Create an Anonymous Session
Call authClient.signIn.anonymous() to create a guest session. This generates a user record with no credentials and returns a fully functional session:
import { authClient } from "@/lib/auth-client";
const { data, error } = await authClient.signIn.anonymous();
if (error) {
console.error(error.message);
} else {
console.log(data.user.id); // "usr_01HXYZ..."
console.log(data.user.isAnonymous); // true
console.log(data.session); // { id, token, expiresAt, ... }
}Check if the Current User Is Anonymous
Use the isAnonymous flag to decide what UI to show -- for example, displaying an upgrade banner or restricting certain features:
const session = await authClient.getSession();
if (session?.data?.user?.isAnonymous) {
// Show upgrade prompt
} else {
// Regular authenticated user
}Upgrade to Email & Password
Link an email and password to the anonymous user, converting them to a full account:
const { data, error } = await authClient.anonymous.upgrade({
email: "user@example.com",
password: "securePassword123",
name: "Jane Doe", // optional
});
if (error) {
// Handle errors:
// - "Email already in use" (409)
// - "Invalid email" (422)
// - "Password too short" (422)
// - "User is not anonymous" (400)
console.error(error.message);
} else {
console.log(data.user.isAnonymous); // false
console.log(data.user.email); // "user@example.com"
// The user ID and session remain the same
}Upgrade via Social OAuth
Anonymous users can also upgrade by linking a social account:
await authClient.anonymous.upgradeWithSocial({
provider: "google",
callbackURL: "/dashboard",
});
// Redirects to Google OAuth flow
// On return, the anonymous user is linked to the Google accountThis works with any social provider you have enabled (Google, GitHub, Apple, etc.).
Complete Component Example
Here is a full React component that wraps your app with anonymous session management and an upgrade banner:
"use client";
import { authClient } from "@/lib/auth-client";
import { useState, useEffect } from "react";
export function AppShell({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
initSession();
}, []);
async function initSession() {
const session = await authClient.getSession();
if (session?.data?.user) {
// User already has a session (anonymous or full)
setUser(session.data.user);
} else {
// No session -- create an anonymous one
const { data } = await authClient.signIn.anonymous();
if (data) setUser(data.user);
}
setLoading(false);
}
if (loading) return <div>Loading...</div>;
return (
<div>
{user?.isAnonymous && <UpgradeBanner />}
{children}
</div>
);
}
function UpgradeBanner() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [showForm, setShowForm] = useState(false);
async function handleUpgrade(e: React.FormEvent) {
e.preventDefault();
setError("");
const { data, error } = await authClient.anonymous.upgrade({
email,
password,
});
if (error) {
setError(error.message ?? "Upgrade failed");
} else {
// Refresh to reflect the upgraded account
window.location.reload();
}
}
async function handleSocialUpgrade(provider: string) {
await authClient.anonymous.upgradeWithSocial({
provider,
callbackURL: window.location.pathname,
});
}
if (!showForm) {
return (
<div style={{
padding: "12px 16px",
backgroundColor: "#f0f4ff",
borderBottom: "1px solid #d0d8f0",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}>
<span>
You are using a guest account. Your data will be preserved
when you create an account.
</span>
<button onClick={() => setShowForm(true)}>
Create Account
</button>
</div>
);
}
return (
<div style={{
padding: "16px",
backgroundColor: "#f0f4ff",
borderBottom: "1px solid #d0d8f0",
}}>
<form onSubmit={handleUpgrade}>
<h3>Create your account</h3>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
{error && <p style={{ color: "red" }}>{error}</p>}
<button type="submit">Save Account</button>
<button type="button" onClick={() => setShowForm(false)}>
Later
</button>
</form>
<div style={{ marginTop: "12px" }}>
<p>Or upgrade with a social account:</p>
<button onClick={() => handleSocialUpgrade("google")}>
Continue with Google
</button>
<button onClick={() => handleSocialUpgrade("github")}>
Continue with GitHub
</button>
</div>
</div>
);
}Tip: Always check for an existing session before calling
signIn.anonymous(). If you create a new anonymous session on every page load, you will generate orphaned user records and hit rate limits.
What's Preserved on Upgrade
When an anonymous user upgrades to a full account, the upgrade is an in-place mutation of the existing user record -- not a creation of a new one. This means all foreign key relationships to the user ID stay intact.
| Data | Preserved? | Notes |
|---|---|---|
| User ID | Yes | The id remains exactly the same |
| Session | Yes | The current session continues; no re-authentication needed |
| Associated records | Yes | Any data linked to the user ID (posts, preferences, cart items, etc.) |
| Organization memberships | Yes | If the anonymous user was added to an organization |
| Metadata | Yes | Any custom metadata set on the user record |
What Changes on Upgrade
| Property | Before Upgrade | After Upgrade |
|---|---|---|
isAnonymous | true | false |
email | null | The provided email |
name | null | The provided name (if given) |
emailVerified | false | false (a verification email is sent) |
Cleanup of Stale Anonymous Accounts
Anonymous users who never upgrade will accumulate over time. You should plan for cleanup from the start.
Automatic Cleanup
You can configure a maximum age for anonymous accounts in your dashboard under Authentication > Methods > Anonymous > Settings, or via the SDK:
await banata.configuration.saveDashboardConfig({
authMethods: {
anonymous: {
enabled: true,
maxAge: 30 * 24 * 60 * 60, // 30 days in seconds
},
},
});When maxAge is set, anonymous users whose last session activity exceeds the threshold are automatically cleaned up. The process:
- Identifies anonymous users whose last session activity exceeds
maxAge. - Deletes the anonymous user record and associated session data.
- Logs a
user.deletedaudit event for each removal.
Manual Cleanup via Admin SDK
If you prefer more control, you can clean up stale anonymous accounts programmatically:
import { BanataAuth } from "@banata-auth/sdk";
const banata = new BanataAuth({
apiKey: "your-api-key",
baseUrl: "https://auth.banata.dev",
});
// List anonymous users
const { data } = await banata.users.listUsers({
filter: { isAnonymous: true },
limit: 100,
});
// Delete stale anonymous users
for (const user of data) {
const age = Date.now() - new Date(user.createdAt).getTime();
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
if (age > thirtyDays) {
await banata.users.deleteUser({ userId: user.id });
}
}Warning: Deleting an anonymous user removes all associated data. If the anonymous user has created content or records in your app that should be preserved, consider reassigning ownership before deletion.
Rate Limits
Anonymous session creation is rate-limited to prevent abuse:
| Operation | Limit |
|---|---|
| Create anonymous session | 30 requests per minute per IP |
If your application exceeds this limit, the API returns a 429 Too Many Requests error. Make sure your frontend checks for an existing session before creating a new anonymous one.
Security Considerations
- Rate limits are your first line of defense. Without them, anonymous auth can be abused to create unlimited user records. The built-in limit of 30 per minute per IP keeps this in check.
- Limit what anonymous users can do. Use the
isAnonymousflag to gate sensitive operations like payments, data exports, and administrative actions. Treat anonymous users as untrusted until they upgrade. - Always configure cleanup. Set a
maxAgeor implement manual cleanup to prevent unbounded growth of anonymous records in your database. - Session expiry still applies. Anonymous sessions expire according to your standard session lifetime (7 days by default). After expiry, the anonymous user cannot resume their session, and their data may become inaccessible if they have no credentials to re-authenticate.
- Prompt upgrades at natural breakpoints. Display clear upgrade prompts after the user completes a meaningful action (finishing a task, before checkout, after saving a document) to maximize conversion from anonymous to full accounts.
Troubleshooting
"User is not anonymous"
You called the upgrade endpoint on a user whose isAnonymous flag is already false. Only anonymous users can be upgraded. If the user already has credentials, they are a full account.
"Email already in use"
The email provided during upgrade belongs to another existing user. An anonymous user cannot claim an email that is already registered. You may want to prompt the user to sign in to the existing account instead.
"Anonymous session expired"
The session expired before the user upgraded. The anonymous user record may still exist, but since there are no credentials to re-authenticate, the user will get a new anonymous session if they return. Data from the previous session may be inaccessible unless you have a recovery mechanism.
"Too many anonymous accounts"
If you see a large number of anonymous user records, check for:
- Bot traffic creating sessions programmatically.
- Missing cleanup configuration -- make sure
maxAgeis set. - Frontend bugs that create a new anonymous session on every page load instead of reusing the existing session.
Next Steps
- Email & Password -- The most common upgrade target for anonymous users.
- Social OAuth -- Let anonymous users upgrade via Google, GitHub, and more.
- Magic Links -- Passwordless upgrade path.
- Organizations -- Add anonymous users to multi-tenant workspaces.