Salta al contenuto principale

Come usare Better Auth in app Capacitor (Guida 2026)

Usa Better Auth in un'app Capacitor 8: trusted origins, bearer token al posto dei cookie, email e password, native Google e Apple sign-in e fix.

Come usare Better Auth in app Capacitor (Guida 2026)

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:

  1. L'app apre l'URL di login di Better Auth nel browser di sistema (ad esempio tramite SocialLogin.openSecureWindow o un plugin per in-app browser).
  2. Better Auth gestisce l'handshake OAuth con il provider e crea la sessione sul server.
  3. La tua callback page converte quella sessione in un token monouso a breve scadenza (Better Auth ha un oneTimeToken plugin per questo) e reindirizza a un deep link come com.example.app://auth?token=....
  4. 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.

Live update per le app Capacitor

Quando un bug del web layer è live, rilascia il fix tramite Capgo invece di aspettare giorni per l'approvazione degli store. Gli utenti ricevono l'update in background, mentre le modifiche native restano nel normale percorso di review.

Ultimi dal Blog

Capgo ti offre i migliori insight per creare un'app mobile davvero professionale.