Per usare Better Auth in un'app Capacitor, installa il better-auth client nel tuo code web, puntalo al tuo server Better Auth e aggiungi le origini Capacitor (capacitor://localhost su iOS, https://localhost su Android) a trustedOrigins, e passa da cookie a bearer token con il bearer plugin. Email e password funzionano tramite SDK senza modifiche. 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 porta un'app web Better Auth su Capacitor 8.
Cosa cambia quando Better Auth gira in Capacitor
Un'app Capacitor è una web app servita da un'origine locale all'interno di una WebView nativa. Rispetto a un normale sito web, ci sono tre differenze:
| 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 lascia l'app bundled, Google blocca i WebViews |
Le soluzioni sono: fidarsi degli origin di Capacitor, usare 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, 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 è importante per Apple. I token dal foglio nativo iOS portano il tuo bundle ID come audience, mentre i token web e Android portano il Services ID. Better Auth usa questo campo per accettarli entrambi.
Monta il handler e consenti CORS dagli origin dell'app. Espone il 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.
Passo 2: Crea il client di autenticazione nell'app
bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync
Salva il token di sessione nel Keychain o Keystore e tieni una copia in memoria in modo che il client possa 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 che la tua app venga renderizzata, 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 subpath corrispondente (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, fai fallback su 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 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 apre 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: accesso nativo con Apple
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 lo vuoi 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 ottieni un idToken, altrove passi l'access token. Il pagina di integrazione Better Auth contiene l'esempio completo per Facebook.
Passo 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, perché la pagina di login del provider non deve caricarsi all'interno della tua WebView. Il pattern che funziona:
- L'app apre l'URL di sign-in di Better Auth nel browser di sistema (ad esempio con
SocialLogin.openSecureWindowo un plugin di browser in-app). - Better Auth esegue la danza OAuth con il provider e crea la sessione sul server.
- La tua pagina callback converte quella sessione in un token one-time 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 gioco rispetto all'handoff dell'ID token. Se ti serve solo l'SSO enterprise e non ti serve il 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 l'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> allo stesso modo in cui accetta il cookie.
Risoluzione problemi
| Problema | Causa | Soluzione |
|---|---|---|
Invalid origin o 403 al sign-in |
Capacitor origine non attendibile | Aggiungi capacitor://localhost e https://localhost a trustedOrigins |
| Errore CORS nella console del WebView | Il server CORS non elenca le origini dell'app | Aggiungili al tuo middleware CORS, consentendo 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'] a CORS |
Google: disallowed_useragent |
Pagina OAuth caricata nella WebView | Usa il sign-in nativo e il idToken handoff |
| ID token di Google rifiutato | L'audience non corrisponde all'ID client configurato | Decodifica il token con SocialLogin.decodeIdToken e confronta aud con clientId |
| ID token di Apple rifiutato su iOS | L'audience è il bundle ID | Imposta appBundleIdentifier nel provider Apple |
| Errore nonce Apple | Nonce diverso inviato ad 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 del lato mobile
Le modifiche all'auth raramente sono native. Le origini fidate e i provider sono configurazione server, e il client code è TypeScript, quindi la maggior parte delle fix può essere rilasciata tramite Live updates di Capgo al posto di una release sullo store. Hai bisogno di un nuovo binario solo quando aggiungi un plugin nativo o modifichi gli URL schemes.
Guide correlate
- Come aggiungere l'autenticazione a un'app Capacitor confronta Better Auth con Firebase, Supabase, Neon Auth (Better Auth gestito) e provider di identità hostati.
- Autenticazione biometrica nelle 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.