Per usare Better Auth in un'app Capacitor, installa il better-auth client nel tuo web code, punta al tuo server Better Auth e aggiungi le Capacitor origins (capacitor://localhost su iOS, https://localhost su Android) per trustedOrigins, e passare dai cookie ai bearer token con il bearer plugin. Email e password funzionano tramite SDK così come sono. Per Google e Apple, ottieni un ID token con un plugin nativo come @capgo/capacitor-social-login e passalo a authClient.signIn.social({ idToken }).
Questa guida mostra la configurazione di server e client, lo storage dei token, il social sign-in, i provider enterprise e gli errori comuni quando si migra un'app web Better Auth in Capacitor 8.
Cosa cambia quando Better Auth gira in Capacitor
Un'app Capacitor è un'app web servita da un'origine locale all'interno di una WebView nativa. Rispetto a un normale sito web, tre cose sono diverse:
| Argomento | Su un sito web | In un'app Capacitor |
|---|---|---|
| Origine | https://app.example.com |
capacitor://localhost (iOS), https://localhost (Android) |
| Auth server | Often same site, cookies just work | Dominio diverso, i cookie di terze parti sono bloccati da WKWebView |
| Redirect OAuth | Il redirect a pagina intera va bene | Il redirect esce dalla tua app bundle, Google blocca le WebView |
Le soluzioni sono: fidarsi degli origin Capacitor, usare i bearer token e usare plugin nativi per il social login.
Passaggio 1: Configura il server Better Auth
Questo esempio usa Hono, ma la configurazione di Better Auth è la stessa per Express, i route handler di Next.js o qualsiasi altro runtime.
bun add better-auth hono
// auth.ts
import { betterAuth } from 'better-auth';
import { bearer } from 'better-auth/plugins';
import { db } from './db'; // your Drizzle, Prisma, Kysely or pg instance
export const auth = betterAuth({
baseURL: process.env.BETTER_AUTH_URL, // https://auth.example.com
database: db,
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_WEB_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
},
apple: {
clientId: process.env.APPLE_SERVICE_ID as string,
clientSecret: process.env.APPLE_CLIENT_SECRET as string,
appBundleIdentifier: process.env.APPLE_BUNDLE_ID as string, // com.example.app
},
},
trustedOrigins: [
'capacitor://localhost', // iOS WebView
'https://localhost', // Android WebView (androidScheme https, the default)
'https://app.example.com', // your web build
'https://appleid.apple.com',
],
plugins: [bearer()],
});
appBundleIdentifier è rilevante per Apple. I token dal sheet nativo iOS riportano il tuo bundle ID come audience, mentre i token web e Android riportano il Services ID. Better Auth usa questo campo per accettarli entrambi.
Monta l'handler e abilita il CORS dagli origin dell'app. Esponi l' set-auth-token header, altrimenti il client non vedrà mai il bearer token:
// server.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { auth } from './auth';
const app = new Hono();
app.use(
'/api/auth/*',
cors({
origin: ['capacitor://localhost', 'https://localhost', 'https://app.example.com'],
allowHeaders: ['Content-Type', 'Authorization'],
allowMethods: ['GET', 'POST', 'OPTIONS'],
exposeHeaders: ['set-auth-token'],
credentials: true,
}),
);
app.on(['GET', 'POST'], '/api/auth/*', (c) => auth.handler(c.req.raw));
export default app;
Esegui bunx @better-auth/cli migrate (o generate per il tuo ORM) per creare le tabelle.
Passaggio 2: Crea l'auth client nell'app
bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync
Salva il session token nel Keychain o Keystore, e tieni una copia in memoria per permettere al client di leggerlo in modo sincrono:
// auth-client.ts
import { createAuthClient } from 'better-auth/client';
import { NativeBiometric } from '@capgo/capacitor-native-biometric';
const TOKEN_KEY = 'session.token';
let sessionToken = '';
export async function loadSessionToken() {
try {
const { value } = await NativeBiometric.getData({ key: TOKEN_KEY });
sessionToken = value;
} catch {
sessionToken = '';
}
}
async function saveSessionToken(token: string) {
sessionToken = token;
if (token) {
await NativeBiometric.setData({ key: TOKEN_KEY, value: token });
} else {
await NativeBiometric.deleteData({ key: TOKEN_KEY }).catch(() => {});
}
}
export const authClient = createAuthClient({
baseURL: 'https://auth.example.com',
fetchOptions: {
auth: {
type: 'Bearer',
token: () => sessionToken,
},
onSuccess: async (ctx) => {
const token = ctx.response.headers.get('set-auth-token');
if (token) await saveSessionToken(token);
},
},
});
export async function clearSession() {
await saveSessionToken('');
}
Chiama await loadSessionToken() prima del render dell'app, quindi authClient.getSession() per ripristinare l'utente:
import { authClient, loadSessionToken } from './auth-client';
await loadSessionToken();
const { data: session } = await authClient.getSession();
if (session) {
console.log('Signed in as', session.user.email);
}
Se usi React, Vue, Svelte o Solid, importa createAuthClient dal relativo subpath (better-auth/react, better-auth/vue, …) per ottenere il useSession hook. Il fetchOptions rimangono invariati. I setData e getData metodi sono solo nativi. Sul web, si usa il fallback con i cookie o sessionStorage usando Capacitor.isNativePlatform().
Passaggio 3: Email e password
Nessun plugin necessario. Sono semplici chiamate HTTP.
import { authClient, clearSession } from './auth-client';
export async function signUp(email: string, password: string, name: string) {
const { data, error } = await authClient.signUp.email({ email, password, name });
if (error) throw new Error(error.message);
return data;
}
export async function signIn(email: string, password: string) {
const { data, error } = await authClient.signIn.email({ email, password });
if (error) throw new Error(error.message);
return data;
}
export async function signOut() {
await authClient.signOut();
await clearSession();
}
Per la verifica dell'email e il reset della password, Better Auth invia link che aprono un URL. Punta callbackURL a una pagina del tuo dominio configurata anche come iOS Universal Link e Android App Link, così il link aprirà l'app sui telefoni e il sito web altrove.
Passaggio 4: Google sign-in nativo
Configura Google come descritto in Come effettuare il login con Google usando Capacitor, quindi:
import { SocialLogin } from '@capgo/capacitor-social-login';
import { authClient } from './auth-client';
await SocialLogin.initialize({
google: {
webClientId: 'WEB_CLIENT_ID.apps.googleusercontent.com',
iOSClientId: 'IOS_CLIENT_ID.apps.googleusercontent.com',
mode: 'online', // Better Auth needs the idToken, offline mode only returns a code
},
});
export async function signInWithGoogle() {
const { result } = await SocialLogin.login({ provider: 'google', options: {} });
if (result.responseType !== 'online' || !result.idToken) {
throw new Error('Google did not return an ID token');
}
const { data, error } = await authClient.signIn.social({
provider: 'google',
idToken: {
token: result.idToken,
accessToken: result.accessToken?.token,
},
});
if (error) throw new Error(error.message);
return data;
}
Better Auth verifica il token con le chiavi di Google e il client ID configurato, crea o collega l'utente e restituisce una sessione. Il bearer token arriva nell' set-auth-token header e il tuo onSuccess hook lo memorizza.
Passaggio 5: Native Apple sign-in
Configura Apple come descritto in Come effettuare il login con Apple usando Capacitor. Genera un nonce, passalo ad Apple e invia lo stesso valore a Better Auth:
import { SocialLogin } from '@capgo/capacitor-social-login';
import { authClient } from './auth-client';
export async function signInWithApple() {
const nonce = crypto.randomUUID();
const { result } = await SocialLogin.login({
provider: 'apple',
options: { scopes: ['email', 'name'], nonce },
});
if (!result.idToken) throw new Error('Apple did not return an ID token');
const { data, error } = await authClient.signIn.social({
provider: 'apple',
idToken: {
token: result.idToken,
nonce,
accessToken: result.accessToken?.token,
},
});
if (error) throw new Error(error.message);
return data;
}
Apple condivide il nome dell'utente solo alla prima autorizzazione. Se vuoi salvarlo nel record utente, aggiorna il profilo subito dopo il primo sign-in con result.profile.givenName e familyName.
Facebook segue lo stesso pattern. Su iOS con Limited Login riceverai un idToken, altrove passi l'access token. Il/La Pagina di integrazione di Better Auth contiene l'esempio completo per Facebook.
Passaggio 6: Okta, Auth0, Entra ID e altri provider OIDC
Il plugin di Better Auth genericOAuth include helper per Auth0, Okta, Keycloak e Microsoft Entra ID, oltre alla configurazione manuale per qualsiasi server OIDC. Nella build web, authClient.signIn.oauth2({ providerId }) funziona come su qualsiasi sito.
Su iOS e Android il flusso richiede il browser di sistema, poiché la pagina di login del provider non deve caricarsi all'interno della WebView. Il pattern consigliato:
- L'app apre l'URL di login di Better Auth nel browser di sistema (ad esempio tramite
SocialLogin.openSecureWindowo un plugin per in-app browser). - Better Auth gestisce l'handshake OAuth con il provider e crea la sessione sul server.
- La tua callback page converte quella sessione in un token monouso a breve scadenza (Better Auth ha un
oneTimeTokenplugin per questo) e reindirizza a un deep link comecom.example.app://auth?token=.... - L'app verifica il token monouso con l'auth client e riceve una normale sessione bearer.
Ci sono più componenti in movimento rispetto all'handoff dell'ID token. Se ti serve solo l'SSO enterprise e non hai bisogno del database utenti di Better Auth, l'accesso diretto al provider è più semplice, vedi le Okta, Auth0 e Entra ID guide.
Passaggio 7: proteggi il tuo API con la sessione
Il tuo API può leggere la sessione dal bearer token con il API server di Better Auth:
import { Hono } from 'hono';
import { auth } from './auth';
const api = new Hono();
api.get('/me/orders', async (c) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers });
if (!session) return c.json({ error: 'unauthorized' }, 401);
return c.json(await listOrders(session.user.id));
});
Il plugin bearer permette di getSession accettare Authorization: Bearer <token> nello stesso modo in cui accetta il cookie.
Troubleshooting
| Problema | Causa | Soluzione |
|---|---|---|
Invalid origin o 403 al login |
Capacitor origin non attendibile | Aggiungi capacitor://localhost e https://localhost a trustedOrigins |
| Errore CORS nella console WebView | Il CORS del server non include le origin dell'app | Aggiungili al middleware CORS, permettendo Authorization |
Il sign-in ha successo ma getSession restituisce null |
Affidarsi ai cookie tra domini diversi | Usa il plugin bearer e invia il token |
set-auth-token è sempre null |
Header non esposto | Aggiungi exposeHeaders: ['set-auth-token'] al CORS |
Google: disallowed_useragent |
Pagina OAuth caricata nella WebView | Usa il sign-in nativo e il idToken handoff |
| Token ID Google rifiutato | L'audience non corrisponde al client ID configurato | Decodifica il token con SocialLogin.decodeIdToken e confronta aud con clientId |
| Token ID Apple rifiutato su iOS | L'audience è il bundle ID | Imposta appBundleIdentifier nel provider Apple |
| Errore nonce Apple | Nonce differente tra Apple e Better Auth | Genera una volta, riusa lo stesso valore |
| Sessione persa dopo il riavvio dell'app | Token mantenuto solo in memoria | Persisti con setData e caricalo prima della prima richiesta |
Deploy della parte mobile
Le modifiche all'auth raramente sono native. Origini e provider fidati sono configurazioni server, e il client code è TypeScript, quindi la maggior parte dei fix può essere inviata tramite Capgo live updates invece di un rilascio sullo store. Ti serve un nuovo binario solo quando aggiungi un plugin nativo o cambi gli URL scheme.
Guide correlate
- Come aggiungere l'autenticazione a un'app Capacitor confronta Better Auth con Firebase, Supabase e identity provider ospitati.
- Autenticazione biometrica in app Capacitor per sbloccare una sessione salvata con Face ID o impronta digitale.
- Plugin Social Login per i flussi nativi di Google, Apple e Facebook.