Authentication
Volcano provides a complete authentication system with multiple sign-in methods, session management, and security features.
Volcano provides a complete authentication system with multiple sign-in methods, session management, and security features. This guide covers everything you need to implement user authentication in your application.
Overview
The SDK supports several authentication methods:
- Email/Password - Traditional sign-up and sign-in with email verification
- OAuth/SSO - Sign in with Google, GitHub, Microsoft, or Apple
- Anonymous Users - Let users explore your app before creating an account
- Magic Links - Passwordless authentication via email (coming soon)
All authentication methods work with the same session system and integrate seamlessly with Row-Level Security.
Email/Password Authentication
Sign Up
Create a new user account with email and password:
const { confirmationRequired, message, error } = await volcano.auth.signUp({
email: 'alice@example.com',
password: 'secure-password-123',
metadata: {
full_name: 'Alice Smith',
avatar_url: 'https://example.com/alice.jpg',
},
});
if (error) {
// Handle specific error cases
if (error.message.includes('already exists')) {
console.error('An account with this email already exists');
} else if (error.message.includes('weak password')) {
console.error('Please choose a stronger password');
} else {
console.error('Sign up failed:', error.message);
}
return;
}
// Sign up is session-less: it returns no user and no session, and the response is
// deliberately identical whether or not the email was already registered. The
// account is created; obtain a session with a separate `signIn`.
if (confirmationRequired) {
console.log(message ?? 'Check your email to confirm your account, then sign in.');
} else {
await volcano.auth.signIn({ email: 'alice@example.com', password: 'secure-password-123' });
}The metadata field is optional and lets you store additional user information like display names, profile pictures, or preferences. This data is stored securely and accessible via user.user_metadata once the user is signed in.
If your project does not require email confirmation and you want the user signed in immediately, pass signInWhenAllowed: true. When confirmation is not required, the SDK then performs the follow-up signIn for you and the response carries a live user/session:
const { user, session, error } = await volcano.auth.signUp({
email: 'alice@example.com',
password: 'secure-password-123',
signInWhenAllowed: true,
});Sign In
Authenticate an existing user:
const { user, session, error } = await volcano.auth.signIn({
email: 'alice@example.com',
password: 'secure-password-123',
});
if (error) {
if (error.message.includes('Invalid credentials')) {
console.error('Incorrect email or password');
} else {
console.error('Sign in failed:', error.message);
}
return;
}
console.log('Signed in as', user.email);After successful sign-in, the SDK automatically:
- Stores the access token and any returned refresh token in localStorage (browser) or memory (server)
- Refreshes the access token when a refresh token is available
- Makes the user available via
volcano.auth.user()
Profile operations (getUser, updateUser, convertAnonymous, and confirmEmailChange) refresh a rejected access token once when the session has a usable refresh token, then replay the original request values.
They do not retry other HTTP failures or ambiguous network failures, and they do not retry under a replacement session.
Authenticated mutations capture the session before reading or serializing your options and metadata.
If a getter or toJSON() replaces the session, the operation returns AuthSessionChangedError before sending the request.
Sign Out
End the current session:
const { error } = await volcano.auth.signOut();
if (!error) {
console.log('Signed out successfully');
}Sign-out revokes the captured session and clears local storage. For credentials received together
from a successful sign-in or validated refresh, it uses the refresh token directly, even if the
access token has expired. For supplied credentials, it revokes the access-token session.
On HTTP 401, that path can refresh once and revoke the same session without adopting the renewed credentials locally.
Calling signOut() without a current session succeeds without a request. If token revocation fails,
the SDK still clears the captured local session and returns the error so the application can report
it. The cleared session no longer contains credentials needed to retry revocation.
Sign-out waits for an already-running refresh and uses its validated credentials for revocation.
Later refresh attempts for that session return AuthRefreshDiscardedError; they do not send a request.
Concurrent sign-out calls share the same revocation result. A separate sign-in or explicit
session adoption remains current and sign-out returns AuthSessionChangedError.
Session Management
Read the Current Session
Read a snapshot of the session already held by the client:
const {
data: { session },
error,
} = await volcano.auth.getSession();
if (!error && session) {
console.log(session.user?.id);
}getSession() reads local SDK state. It does not refresh or validate the access token; use
getUser() when server validation is required.
Start with a Supplied Access Token
Create a separate client for each server request that carries a user's access token:
import { VolcanoClient } from '@volcano.dev/sdk';
export async function loadRequestUser(accessToken) {
if (typeof accessToken !== 'string' || !accessToken.trim()) {
throw new Error('An access token from the current request is required');
}
const client = new VolcanoClient({
anonKey: process.env.VOLCANO_ANON_KEY,
accessToken,
});
const { user, error } = await client.auth.getUser();
if (error) throw error;
return user;
}Call this helper from your request handler with the bearer token from that request.
For a Volcano function, use the access token in event.__volcano_auth.access_token supplied for that invocation.
The helper validates the token with Volcano before returning the user.
Refresh must preserve the server session identified by the access JWT, even before a profile is loaded. A different session is rejected, including another session for the same user. Supplied credentials need a readable session identifier to refresh, even when you provide a user profile or load it from the server. Profile data does not prove that access and refresh tokens belong together.
Once a user identity has been validated, a refresh response for another user is also rejected.
Construction makes no request and does not persist the supplied credentials.
getSession() initially returns the access token with refresh_token: null and user: null.
A successful getUser() caches the server-validated profile without changing the token.
Without a refresh token, an HTTP 401 remains an authentication error, refreshSession() returns an error, and signOut() revokes the server session identified by the access token before clearing local state.
Pass refreshToken alongside accessToken when the client should refresh that session.
setSession() still requires a complete session.
Adopt an Existing Session
Copy a complete session into another client:
const {
data: { session: sourceSession },
} = await source.auth.getSession();
if (sourceSession?.refresh_token && sourceSession.user) {
const { data, error } = await target.auth.setSession({
...sourceSession,
refresh_token: sourceSession.refresh_token,
user: sourceSession.user,
});
if (!error) {
console.log(data.session.user.id);
}
}setSession() validates and copies the supplied session into the target client's memory. It does
not contact Volcano, persist tokens, or notify auth listeners. Call it again after creating a new
client or reloading the page.
Check Current User
Get the current user synchronously (returns cached data):
const user = volcano.auth.user();
if (user) {
console.log('Logged in as:', user.email);
console.log('User ID:', user.id);
console.log('Custom data:', user.user_metadata);
} else {
console.log('Not logged in');
}Fetch Fresh User Data
Get the latest user data from the server:
const { user, error } = await volcano.auth.getUser();
if (user) {
console.log('User data refreshed');
console.log('Email:', user.email);
console.log('Created:', user.created_at);
}This is useful when you need to ensure the user data is current, such as after updating their profile elsewhere.
Restore Session on Page Load
When your application starts, restore any existing session:
// In your app initialization
const { user, error } = await volcano.initialize();
if (user) {
console.log('Session restored for', user.email);
// User is authenticated, show dashboard
} else {
// No session, show login page
}The client also completes browser redirect hand-offs automatically. Managed email/password pages can return a session fragment, while OAuth providers return a short-lived authorization code that the SDK exchanges before an authenticated call proceeds.
Listen for Auth State Changes
React to authentication events in real-time:
const unsubscribe = volcano.auth.onAuthStateChange((user) => {
if (user) {
// User signed in or session restored
updateUI({ isLoggedIn: true, user });
} else {
// User signed out
updateUI({ isLoggedIn: false, user: null });
}
});
// Clean up when your component unmounts
// unsubscribe();This callback fires immediately with the current state, then again whenever the auth state changes.
Manual Token Refresh
Refresh the current session with its refresh token:
const { session, error } = await volcano.auth.refreshSession();
if (error) {
console.error('Refresh failed:', error.message);
} else {
console.log('Token refreshed, expires in', session.expires_in, 'seconds');
}On success, refreshSession() returns the refreshed token fields in the existing
{ session, error } response and replaces the client's current session. Read the full snapshot,
including its user, with getSession().
If Volcano rejects the refresh token with 401 or 403, the SDK clears that session. A transport
error or server failure leaves the current session unchanged so the application can retry. A late
refresh response never replaces a newer session.
Authenticated profile, session-list/deletion, email-change request/cancellation, linked-provider, provider-token, and provider-API operations refresh a usable session once after HTTP 401 and replay the original request values. They preserve an explicitly replaced session and do not retry other HTTP failures or ambiguous network failures. Deleting the current server session also clears its refreshed local credentials; a separately adopted session remains current.
Hosted Auth Pages (Managed Login)
Instead of building your own login UI, you can redirect users to Volcano's hosted (managed) login and sign-up pages. Always start the flow with signInWithHostedAuth() (or getHostedAuthUrl()): it stores a one-time nonce and passes it as state, which the SDK validates when the session comes back. This binds the returned session to the flow you started and prevents an attacker from tricking a user into adopting an attacker-controlled session (login CSRF / session fixation).
// On your login button/page:
volcano.auth.signInWithHostedAuth(); // or { action: 'signup' }
// You can also get the URL without navigating:
// const url = volcano.auth.getHostedAuthUrl();
// window.location.assign(url);After a successful login or sign-up, Volcano redirects the user back to your configured post_auth_redirect_url with the session in the URL fragment:
https://your-app.com/callback#access_token=...&refresh_token=...&token_type=bearer&expires_in=...&state=<nonce>The SDK handles this hand-off for you. On your redirect page, just create your client — the session's state is validated against the stored nonce, then it's detected, stored, and stripped from the URL automatically. You never parse the fragment yourself:
// On your post-auth redirect page (e.g. /callback)
const volcano = new VolcanoAuth({
apiUrl: 'https://api.example.com',
anonKey: 'ak-your-anon-key',
});
// The redirect session was already adopted when the client was created,
// so the user is authenticated immediately.
const { user, error } = await volcano.auth.getUser();
if (error) throw error;
console.log('Signed in as', user.email);Security: a fragment whose
statedoesn't match a nonce this client stored (e.g. an unsolicited#access_token=...link, or a flow not started via the SDK) is rejected and scrubbed from the URL — it will not authenticate the user.
Because the session is adopted at construction (the same as a session restored from storage), you don't have to call getUser() before making authenticated requests — any authenticated call works right away:
const volcano = new VolcanoAuth({ apiUrl, anonKey }); // session adopted from URL
// Authenticated immediately — no getUser() required first.
const { user } = await volcano.auth.updateUser({ metadata: { onboarded: true } });
const { data } = await volcano.database('app-db').from('orders').select('*');Notes:
- This is browser-only (it reads
window.location.hash). - The tokens are removed from the URL automatically so they don't linger in browser history or get bookmarked.
- A normal app fragment (for example
#/dashboard) is ignored and left untouched. getUser()andinitialize()also re-check the URL, so they still work if the client was created before the redirect fragment was present (e.g. in a single-page app).
OAuth/SSO Authentication
Sign users in with their existing accounts from popular providers.
Available Providers
google- Google accountsgithub- GitHub accountsmicrosoft- Microsoft/Azure AD accountsapple- Apple ID
Sign In with OAuth
Each provider has a convenience method:
// Sign in with Google
volcano.auth.signInWithGoogle();
// Sign in with GitHub
volcano.auth.signInWithGitHub();
// Sign in with Microsoft
volcano.auth.signInWithMicrosoft();
// Sign in with Apple
volcano.auth.signInWithApple();
// Or use the generic method with any provider
volcano.auth.signInWithOAuth('google');These methods redirect the user to the provider's login page. After successful authentication, they're redirected back to your application with an active session.
Note: OAuth sign-in only works in browser environments. Calling these methods on the server will throw an error. See the Next.js guide for server-side OAuth handling.
Handle OAuth Callback
The SDK requests the platform's authorization-code callback mode. After the OAuth redirect, the user returns with a short-lived, single-use code and the flow's state nonce. The SDK validates the nonce, removes both values from the URL, and exchanges the code for a session before initialize(), getUser(), or another authenticated operation proceeds. In this mode, access and refresh tokens are never placed in the OAuth callback URL. Platform deployments retain the established session-fragment response for older clients that do not request authorization-code mode.
When the exchange uses an eligible HttpOnly cookie session, its response can omit the refresh token. The SDK keeps the access token, clears any stale stored refresh token, and reports refresh_token: null from getSession().
By default the user returns to the page that called signInWithOAuth(); pass { redirectTo } to override:
// On your callback page or app initialization
const { user, error } = await volcano.initialize();
if (user) {
console.log('OAuth sign-in successful:', user.email);
}The callback URL must exactly match one of the project's registered redirect URLs, including its query string. Callback URLs cannot use the reserved OAuth response query parameters code, state, error, error_description, error_uri, iss, or vh_state.
Link OAuth Provider to Existing Account
Let users add OAuth sign-in to an existing email/password account:
// User must be signed in first
const { data, error } = await volcano.auth.linkOAuthProvider('google');
if (error) {
console.error('Failed to link provider:', error.message);
return;
}
// Redirect user to complete linking
window.location.href = data.authorization_url;The result is discarded with AuthSessionChangedError if the active session changes while the
request is in flight.
Unlink OAuth Provider
Remove an OAuth provider from an account:
const { error } = await volcano.auth.unlinkOAuthProvider('google');
if (!error) {
console.log('Google account unlinked');
}The result is discarded with AuthSessionChangedError if the active session changes while the
request is in flight.
List Linked Providers
See which OAuth providers are connected to the current account:
const { providers, error } = await volcano.auth.getLinkedOAuthProviders();
if (providers) {
providers.forEach((p) => {
console.log(`${p.provider} linked on ${p.linked_at}`);
});
}The result is discarded with AuthSessionChangedError if the active session changes while the
request is in flight.
Check Provider Token Status
Check whether Volcano has a valid server-held provider token:
const { provider, expiresIn, error } = await volcano.auth.getOAuthProviderToken('github');This method returns provider and expiry metadata, not the credential. Volcano refreshes an expired
token on the server. A stale result returns AuthSessionChangedError.
Refresh a provider token explicitly when the application needs a new validity window:
const { provider, expiresIn, error } = await volcano.auth.refreshOAuthToken('github');The refresh credential and new access token remain on the server. A stale result returns
AuthSessionChangedError.
Access Provider APIs
After OAuth sign-in, you can make authenticated requests to the provider's API:
// Get the user's GitHub repositories
const { data, error } = await volcano.auth.callOAuthAPI('github', {
endpoint: '/user/repos',
method: 'GET',
});
if (error) {
console.error('Provider request failed:', error);
} else if (Array.isArray(data)) {
data.forEach((repo) => {
console.log(repo.full_name);
});
}Volcano automatically handles token refresh and passes the correct credentials to the provider.
data is the provider's raw JSON value, or null when the provider returns no body.
Provider bodies are limited to 8 MiB after decompression. Transport failures,
invalid JSON, and oversized responses produce a Volcano 502 error.
Check error for Volcano request failures. The current SDK does not expose the
provider's HTTP status: a valid JSON response from a provider, including a 4xx or
5xx response, is returned as data with error: null. Inspect the provider's
documented response shape before treating the operation as successful.
The response is discarded with AuthSessionChangedError if the active session changes while the
request is in flight.
Anonymous Users
Let users explore your app without creating an account, then convert them to full users later.
Create Anonymous User
const { user, session, error } = await volcano.auth.signInAnonymously({
preferred_theme: 'dark',
});
if (user) {
console.log('Anonymous user created:', user.id);
// User can now use the app with limited features
}Anonymous users get a unique ID and can store data, but they don't have an email address or password.
They cannot recover the account after signing out unless they first convert it to a full account.
signUpAnonymous() remains available as a compatibility alias.
Convert to Full Account
When an anonymous user is ready to create a real account:
const { user, error } = await volcano.auth.convertAnonymous({
email: 'alice@example.com',
password: 'secure-password-123',
metadata: {
full_name: 'Alice Smith',
},
});
if (user) {
console.log('Account converted! Welcome,', user.email);
// All their data is preserved with the same user ID
}The conversion preserves the user's ID and all associated data.
Email Verification
If your project requires email verification, users receive a confirmation email after signing up.
Confirm Email
When the user clicks the confirmation link, extract the token and confirm:
// Token comes from the URL query parameter
const token = new URLSearchParams(window.location.search).get('token');
const { message, error } = await volcano.auth.confirmEmail(token);
if (error) {
console.error('Confirmation failed:', error.message);
} else {
console.log('Email confirmed!');
}Confirmation does not sign in the confirmed account or change an unrelated local session.
Resend Confirmation Email
If the user didn't receive the email:
const { message, error } = await volcano.auth.resendConfirmation('alice@example.com');
if (!error) {
console.log('Confirmation email sent');
}The response does not reveal whether the account exists or is already confirmed. Delivery occurs only for an existing unconfirmed account when transactional email is configured.
Password Recovery
Request Password Reset
Request a password reset email. Volcano sends it when transactional email is configured:
const { message, error } = await volcano.auth.resetPasswordForEmail('alice@example.com');
if (!error) {
console.log('Password reset email sent');
}For security, this always succeeds even if the email doesn't exist in your system.
forgotPassword() remains available as a deprecated compatibility alias.
Reset Password
When the user clicks the reset link, extract the token and set a new password:
const token = new URLSearchParams(window.location.search).get('token');
const { message, error } = await volcano.auth.resetPassword({
token,
newPassword: 'new-secure-password-456',
});
if (error) {
console.error('Password reset failed:', error.message);
} else {
console.log('Password updated successfully');
}Resetting the password revokes the recovered account's existing sessions and does not sign it in. The client keeps any unrelated local session unchanged; sign in with the new password when the reset flow completes.
Email Change
Request Email Change
Allow users to change their email address:
const { message, newEmail, error } = await volcano.auth.requestEmailChange('newemail@example.com');
if (!error) {
console.log('Confirmation email sent to', newEmail);
}If another authentication operation replaces the active session before a successful acknowledgement
arrives, the method returns an AuthSessionChangedError and leaves the newer session untouched. The
server may still have started the email change for the original account, so handle this result
separately from a rejected request.
Confirm Email Change
After the user confirms via email:
const token = new URLSearchParams(window.location.search).get('token');
const { user, error } = await volcano.auth.confirmEmailChange(token);
if (user) {
console.log('Email updated to', user.email);
}The returned user reflects the confirmed address. If another authentication operation replaces the
active session before a successful confirmation arrives, the method returns an
AuthSessionChangedError and leaves the newer session's user untouched.
Cancel Email Change
If the user changes their mind:
const { message, error } = await volcano.auth.cancelEmailChange();
if (!error) {
console.log('Email change cancelled');
}If another authentication operation replaces the active session before a successful cancellation
arrives, the method returns an AuthSessionChangedError and leaves the newer session untouched. The
server may still have cancelled the pending change for the original account.
Update User Profile
Modify the current user's password or metadata:
const { user, error } = await volcano.auth.updateUser({
password: 'new-password-789', // Optional
metadata: {
full_name: 'Alice Johnson',
avatar_url: 'https://example.com/alice-new.jpg',
notification_preferences: {
email: true,
push: false,
},
},
});
if (user) {
console.log('Profile updated');
}Multi-Device Session Management
Users can manage their active sessions across devices.
List Sessions
Get all active sessions for the current user:
const { sessions, total, error } = await volcano.auth.getSessions({
page: 1,
limit: 20,
});
if (sessions) {
sessions.forEach((session) => {
console.log('---');
console.log('Device:', session.user_agent);
console.log('IP:', session.ip_address);
console.log('Last active:', session.last_activity_at);
console.log('Current session:', session.is_current);
});
}getSessions() uses offset pagination and defaults to page 1 with 20 sessions per page.
The response is rejected with AuthSessionChangedError if another authentication operation
replaces the local session while the request is in flight.
Revoke a Session
Sign out from a specific device:
const { error } = await volcano.auth.deleteSession(sessionId);
if (!error) {
console.log('Session revoked');
}The request uses the current access token. Deleting that token's own session clears local
credentials, including when the request outcome is uncertain; deleting another session preserves
them. If another authentication operation replaces the session before deletion finishes, the method
returns an AuthSessionChangedError instead of clearing the replacement or acknowledging a stale
result.
Sign Out All Other Devices
Keep only the current session active:
const { error } = await volcano.auth.deleteAllOtherSessions();
if (!error) {
console.log('All other sessions have been signed out');
}The session whose access token authorizes the request remains active. Do not replace the client's
session while this request is in flight: the server may revoke that replacement as an "other"
session. If replacement occurs, the method returns an AuthSessionChangedError instead of
acknowledging a stale result.
Security Best Practices
Protect Service Keys
The SDK prevents service keys (starting with sk-) from being used in browser environments. Service keys bypass Row-Level Security and should only be used in secure server-side code:
// This will throw an error in the browser:
const volcano = new VolcanoAuth({
apiUrl: 'https://api.example.com',
anonKey: 'sk-service-key-here', // ERROR in browser!
});Use HTTPS
Always use HTTPS URLs for your API endpoint to protect tokens in transit.
Secure Password Requirements
Enforce strong passwords in your UI before calling the SDK:
function validatePassword(password) {
if (password.length < 8) {
return 'Password must be at least 8 characters';
}
if (!/[A-Z]/.test(password)) {
return 'Password must contain an uppercase letter';
}
if (!/[0-9]/.test(password)) {
return 'Password must contain a number';
}
return null;
}
const passwordError = validatePassword(password);
if (passwordError) {
showError(passwordError);
return;
}
// Password is valid, proceed with sign up
await volcano.auth.signUp({ email, password });Handle Token Expiration
If an authenticated request receives HTTP 401 and cannot refresh successfully, its error retains status: 401 and any server code and retryAfter metadata. The compatible message remains Session expired. A network failure has no HTTP status.
Access tokens expire after a configured time (default: 1 hour). The SDK handles refresh automatically, but you should handle the case where refresh fails:
volcano.auth.onAuthStateChange((user) => {
if (!user) {
// Session expired or user signed out
redirectToLogin();
}
});Next Steps
- Database - Query your PostgreSQL database with Row-Level Security
- OAuth APIs - Use OAuth tokens to access provider APIs
- Next.js - Handle authentication in server components and middleware