Pour utiliser Better Auth dans une app Capacitor, installez le better-auth client dans votre code web, pointez-le vers votre serveur Better Auth, ajoutez les origines Capacitor (capacitor://localhost sur iOS, https://localhost sur Android) pour trustedOrigins, et passer des cookies aux bearer tokens avec le bearer plugin. L’authentification par e-mail et mot de passe fonctionne telle quelle via le SDK. Pour Google et Apple, obtenez un ID token avec un plugin natif tel que @capgo/capacitor-social-login et transmettez-l’à authClient.signIn.social({ idToken }).
Ce guide présente la configuration serveur et client, le stockage des tokens, la connexion sociale, les fournisseurs enterprise et les erreurs que l’on rencontre en portant une application web Better Auth vers Capacitor 8.
Ce qui change quand Better Auth tourne dans Capacitor
Une application Capacitor est une application web servie depuis une origine locale dans un WebView natif. Par rapport à un site web classique, trois aspects diffèrent :
| Sujet | Sur un site web | Dans une application Capacitor |
|---|---|---|
| Origine | https://app.example.com |
capacitor://localhost (iOS), https://localhost (Android) |
| Serveur d’auth | Souvent le même site : les cookies fonctionnent | Domaine différent, les cookies tiers sont bloqués par WKWebView |
| redirections OAuth | Une redirection pleine page suffit | La redirection quitte votre app bundle, Google bloque les WebViews |
Les solutions sont : faire confiance aux origines Capacitor, utiliser des bearer tokens et utiliser des plugins natifs pour le social login.
Étape 1 : configurer le serveur Better Auth
Cet exemple utilise Hono, mais la config Better Auth est la même pour Express, les route handlers Next.js ou tout autre 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 est important pour Apple. Les tokens issus de la feuille iOS native portent votre bundle ID comme audience, tandis que les tokens web et Android portent le Services ID. Better Auth utilise ce champ pour accepter les deux.
Montez le handler et autorisez le CORS depuis les origines de l'app. Exposez l' set-auth-token en-tête, sinon le client ne verra jamais le 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;
Exécutez bunx @better-auth/cli migrate (ou generate pour votre ORM) pour créer les tables.
Étape 2 : Créer le client d'auth dans l'app
bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync
Stockez le token de session dans le Keychain ou le Keystore, et gardez une copie en mémoire pour que le client puisse le lire de manière synchrone :
// 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('');
}
Appelez await loadSessionToken() avant le rendu de votre app, puis authClient.getSession() pour restaurer l'utilisateur :
import { authClient, loadSessionToken } from './auth-client';
await loadSessionToken();
const { data: session } = await authClient.getSession();
if (session) {
console.log('Signed in as', session.user.email);
}
Si vous utilisez React, Vue, Svelte ou Solid, importez createAuthClient depuis le sous-chemin correspondant (better-auth/react, better-auth/vue, …) pour obtenir le useSession hook. Le fetchOptions restent identiques. Le setData et getData Les méthodes sont uniquement natives. Sur le web, utilisez des cookies ou sessionStorage en utilisant Capacitor.isNativePlatform().
Étape 3 : E-mail et mot de passe
Aucun plugin nécessaire. Ce sont de simples appels 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();
}
Pour la vérification d’e-mail et la réinitialisation du mot de passe, Better Auth envoie des liens qui ouvrent une URL. Point callbackURL vers une page de votre domaine également configurée comme Universal Link iOS et App Link Android, pour que le lien ouvre l’application sur mobile et le site web ailleurs.
Étape 4 : Connexion Google native
Configurez Google comme décrit dans Comment se connecter avec Google via Capacitor, puis :
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 vérifie le token par rapport aux clés de Google et à l'ID client configuré, crée ou lie l'utilisateur, puis renvoie une session. Le bearer token arrive dans l' set-auth-token header et votre onSuccess hook le stocke.
Étape 5 : Connexion native Apple
Configurez Apple comme décrit dans Comment se connecter avec Apple via Capacitor. Générez un nonce, transmettez-l’à Apple et envoyez la même valeur à 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 ne partage le nom de l'utilisateur que lors de la première autorisation. Si vous le voulez sur la fiche utilisateur, mettez à jour le profil juste après la première connexion avec result.profile.givenName et familyName.
Facebook suit le même schéma. Sur iOS avec Limited Login, vous obtenez un idToken, ailleurs vous passez le token d'accès. La page d'intégration Better Auth contient l'exemple Facebook complet.
Étape 6 : Okta, Auth0, Entra ID et autres fournisseurs OIDC
de Better Auth genericOAuth le plugin propose des helpers pour Auth0, Okta, Keycloak et Microsoft Entra ID, ainsi qu'une config manuelle pour tout serveur OIDC. Sur le build web, authClient.signIn.oauth2({ providerId }) fonctionne comme sur n'importe quel site.
Sur iOS et Android, le flux nécessite le navigateur système, car la page de connexion du fournisseur ne doit pas se charger dans votre WebView. Le pattern qui fonctionne :
- L'application ouvre votre URL de connexion Better Auth dans le navigateur système (par exemple avec
SocialLogin.openSecureWindowou un plugin de navigateur in-app). - Better Auth gère le flux OAuth avec le fournisseur et crée la session sur le serveur.
- Votre page de callback convertit cette session en un token éphémère à usage unique (Better Auth possède un
oneTimeTokenplugin pour cela) et redirige vers un deep link tel quecom.example.app://auth?token=.... - L'application vérifie le token à usage unique avec le client d'authentification et reçoit une session bearer classique.
Cela fait plus d'éléments à gérer que le transfert de token ID. Si vous avez seulement besoin du SSO entreprise sans la base de données utilisateur de Better Auth, s'authentifier directement auprès du fournisseur est plus simple, consultez les Okta, Auth0 et Entra ID guides.
Étape 7 : Protégez votre API avec la session
Votre propre API peut lire la session à partir du bearer token avec l’API serveur de 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));
});
Le plugin bearer permet à getSession d'accepter Authorization: Bearer <token> de la même manière qu'il accepte le cookie.
Troubleshooting
| Problème | Cause | Corriger |
|---|---|---|
Invalid origin ou 403 lors de la connexion |
Capacitor origine non approuvée | Ajoutez capacitor://localhost et https://localhost vers trustedOrigins |
| Erreur CORS dans la console WebView | Le CORS du serveur ne liste pas les origines de l'app | Ajoutez-les à votre middleware CORS, autorisez Authorization |
La connexion réussit mais getSession renvoie null |
S'appuyer sur les cookies entre domaines | Utilisez le plugin bearer et envoyez le token |
set-auth-token est toujours null |
Header non exposé | Ajoutez exposeHeaders: ['set-auth-token'] au CORS |
Google : disallowed_useragent |
Page OAuth chargée dans la WebView | Utilisez la connexion native et le idToken Transfert |
| Token ID Google rejeté | L'audience ne correspond pas au client ID configuré | Décoder le token avec SocialLogin.decodeIdToken et comparer aud par clientId |
| Token ID Apple rejeté sur iOS | L'audience est le bundle ID | Définissez appBundleIdentifier dans le provider Apple |
| Erreur de nonce Apple | Nonce différent envoyé à Apple et Better Auth | Générez une fois, réutilisez la même valeur |
| Session perdue au redémarrage de l'app | Token conservé uniquement en mémoire | Persister avec setData et le charger avant la première requête |
Déploiement de la partie mobile
Les changements d'auth sont rarement natifs. Les origines et les providers de confiance se configurent côté serveur, et le client code est en TypeScript, donc la plupart des correctifs peuvent être déployés via les live updates Capgo au lieu d'une nouvelle version sur les stores. Un nouveau binaire n'est nécessaire que si vous ajoutez un plugin natif ou modifiez les URL schemes.
Guides associés
- Comment ajouter l'authentification à une application Capacitor compare Better Auth avec Firebase, Supabase, Neon Auth (Better Auth managé) et les fournisseurs d'identité hébergés.
- L'authentification biométrique dans les apps Capacitor pour déverrouiller une session stockée avec Face ID ou une empreinte digitale.
- Plugin Social Login pour les flux natifs Google, Apple et Facebook.