Para usar Better Auth en una app de Capacitor, instala el better-auth cliente en tu code web, apúntalo a tu servidor de Better Auth y añade los Capacitor origins (capacitor://localhost en iOS, https://localhost en Android) para trustedOrigins, y cambiar de cookies a tokens bearer con el bearer plugin. El correo electrónico y la contraseña funcionan a través de SDK tal cual. Para Google y Apple, obtén un ID token con un plugin nativo como @capgo/capacitor-social-login y pásalo a authClient.signIn.social({ idToken }).
Esta guía muestra la configuración del servidor y el cliente, el almacenamiento de tokens, el inicio de sesión social, los proveedores empresariales y los errores que suelen aparecer al migrar una aplicación web de Better Auth a Capacitor 8.
Qué cambia al ejecutar Better Auth dentro de Capacitor
Una app de Capacitor es una aplicación web servida desde un origen local dentro de un WebView nativo. En comparación con un sitio web normal, hay tres diferencias:
| Tema | En un sitio web | En una app de Capacitor |
|---|---|---|
| Origen | https://app.example.com |
capacitor://localhost (iOS), https://localhost (Android) |
| Servidor de auth | Often same site, cookies just work | Dominio distinto, WKWebView bloquea las cookies de terceros |
| Redirects de OAuth | El redirect a pantalla completa funciona bien | El redirect sale de tu app empaquetada, Google bloquea WebViews |
Las soluciones son: confiar en los orígenes de Capacitor, usar bearer tokens y usar plugins nativos para social login.
Paso 1: Configurar el servidor de Better Auth
Este ejemplo usa Hono, pero la config de Better Auth es igual para Express, route handlers de Next.js o cualquier otro 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 importa para Apple. Los tokens del sheet nativo de iOS llevan tu bundle ID como audience, mientras que los de web y Android llevan el Services ID. Better Auth usa este campo para aceptar ambos.
Monta el handler y permite CORS desde los orígenes de la app. Expón el set-auth-token header, de lo contrario el cliente nunca verá el 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;
Ejecuta bunx @better-auth/cli migrate (o generate para tu ORM) para crear las tablas.
Paso 2: Crea el cliente de auth en la app
bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync
Guarda el token de sesión en el Keychain o Keystore, y mantén una copia en memoria para que el cliente pueda leerlo de forma síncrona:
// 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('');
}
Llama a await loadSessionToken() antes de que tu app se renderice, luego authClient.getSession() para restaurar al usuario:
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 usas React, Vue, Svelte o Solid, importa createAuthClient desde la subruta correspondiente (better-auth/react, better-auth/vue, …) para obtener el useSession hook. El fetchOptions permanecen iguales. El setData y getData métodos son solo nativos. En web, usa cookies o sessionStorage usando Capacitor.isNativePlatform().
Paso 3: Email y contraseña
No se necesita plugin. Son llamadas HTTP estándar.
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();
}
Para la verificación de correo y el restablecimiento de contraseña, Better Auth envía enlaces que abren una URL. Apunta callbackURL a una página en tu dominio que también esté configurada como Universal Link de iOS y App Link de Android, para que el enlace abra la app en móviles y el sitio web en otros dispositivos.
Paso 4: Inicio de sesión nativo con Google
Configura Google como se describe en Cómo iniciar sesión con Google usando Capacitor, luego:
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 el token contra las claves de Google y el client ID configurado, crea o vincula el usuario y devuelve una sesión. El bearer token llega en la set-auth-token cabecera y tu onSuccess hook lo almacena.
Paso 5: Inicio de sesión nativo con Apple
Configura Apple como se describe en Cómo iniciar sesión con Apple usando Capacitor. Genera un nonce, pásalo a Apple y envía el mismo valor 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 solo comparte el nombre del usuario en la primera autorización. Si quieres guardarlo en el registro del usuario, actualiza el perfil justo después del primer inicio de sesión con result.profile.givenName y familyName.
Facebook sigue el mismo patrón. En iOS con Limited Login obtienes un idToken, en otros casos pasas el access token. La página de integración de Better Auth tiene el ejemplo completo de Facebook.
Paso 6: Okta, Auth0, Entra ID y otros proveedores OIDC
de Better Auth genericOAuth El plugin incluye helpers para Auth0, Okta, Keycloak y Microsoft Entra ID, además de configuración manual para cualquier servidor OIDC. En la build web, authClient.signIn.oauth2({ providerId }) funciona como en cualquier sitio.
En iOS y Android, el flujo requiere el navegador del sistema, ya que la página de inicio de sesión del proveedor no debe cargarse dentro de tu WebView. El patrón que funciona:
- La app abre la URL de inicio de sesión de Better Auth en el navegador del sistema (por ejemplo, con
SocialLogin.openSecureWindowo un plugin de navegador in-app). - Better Auth ejecuta el flujo OAuth con el proveedor y crea la sesión en el servidor.
- Tu página de callback convierte esa sesión en un token de un solo uso de vida corta (Better Auth tiene
oneTimeTokenplugin para esto) y redirige a un deep link comocom.example.app://auth?token=.... - La app verifica el token de un solo uso con el cliente de autenticación y recibe una sesión bearer normal.
Eso implica más piezas móviles que la transferencia del token de ID. Si solo necesitas SSO empresarial y no necesitas la base de datos de usuarios de Better Auth, iniciar sesión directamente contra el proveedor es más simple, ver la Okta, Auth0 y Entra ID guías.
Paso 7: Protege tu API con la sesión
Tu propio API puede leer la sesión desde el bearer token con el servidor API 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));
});
El plugin bearer hace que getSession aceptan Authorization: Bearer <token> de la misma forma en que acepta la cookie.
Solución de problemas
| Problema | Causa | Solución |
|---|---|---|
Invalid origin o un 403 al iniciar sesión |
origen Capacitor no confiable | Añadir capacitor://localhost y https://localhost a trustedOrigins |
| error de CORS en la consola del WebView | el CORS del servidor no incluye los orígenes de la app | Añádelos a tu middleware de CORS, permite Authorization |
El inicio de sesión se completa, pero getSession devuelve null |
Depender de cookies entre dominios | Usa el plugin bearer y envía el token |
set-auth-token siempre es null |
Header no expuesto | Añadir exposeHeaders: ['set-auth-token'] a CORS |
Google: disallowed_useragent |
Página de OAuth cargada en el WebView | Usa el inicio de sesión nativo y el idToken handoff |
| Token de ID de Google rechazado | La audiencia no coincide con el client ID configurado | Decodifica el token con SocialLogin.decodeIdToken y compara aud con clientId |
| Token de ID de Apple rechazado en iOS | Audience es el bundle ID | Configurar appBundleIdentifier en el proveedor de Apple |
| Error de nonce de Apple | Nonce diferente enviado a Apple y Better Auth | Genera una vez, reutiliza el mismo valor |
| Sesión perdida tras reiniciar la app | Token guardado solo en memoria | Persistir con setData y cargarlo antes de la primera petición |
Desplegando la parte móvil
Los cambios de Auth rara vez son nativos. Los orígenes y proveedores de confianza son configuración del servidor, y el cliente code es TypeScript, así que la mayoría de los fixes se pueden enviar mediante actualizaciones Live Update de Capgo en lugar de un release en la tienda. Solo necesitas un nuevo binario cuando añades un plugin nativo o cambias los URL schemes.
Guías relacionadas
- Cómo añadir autenticación a una app Capacitor compara Better Auth con Firebase, Supabase, Neon Auth (Better Auth gestionado) y proveedores de identidad alojados.
- Autenticación biométrica en apps de Capacitor para desbloquear una sesión guardada con Face ID o huella dactilar.
- Plugin de Social Login para los flujos nativos de Google, Apple y Facebook.