Saltar al contenido principal

Cómo usar Better Auth en apps Capacitor (Guía 2026)

Usa Better Auth en una app de Capacitor 8: orígenes de confianza, bearer tokens en lugar de cookies, email y contraseña, inicio de sesión nativo con Google y Apple, y correcciones.

Pantalla de registro de una app Capacitor con Better Auth

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:

  1. La app abre la URL de inicio de sesión de Better Auth en el navegador del sistema (por ejemplo, con SocialLogin.openSecureWindow o un plugin de navegador in-app).
  2. Better Auth ejecuta el flujo OAuth con el proveedor y crea la sesión en el servidor.
  3. Tu página de callback convierte esa sesión en un token de un solo uso de vida corta (Better Auth tiene oneTimeToken plugin para esto) y redirige a un deep link como com.example.app://auth?token=....
  4. 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.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.