Zum Hauptinhalt springen

So nutzt du Better Auth in Capacitor Apps (Guide 2026)

Better Auth in einer Capacitor 8 App nutzen: Trusted Origins, Bearer Tokens statt Cookies, E-Mail und Passwort, natives Google- und Apple-Sign-in sowie Fixes.

Sign-up-Screen einer Capacitor App mit Better Auth

Um Better Auth in einer Capacitor App zu nutzen, installiere den better-auth Client in deinem Web-code, verbinde ihn mit deinem Better Auth Server und füge die Capacitor Origins hinzu (capacitor://localhost unter iOS, https://localhost auf Android) zu trustedOrigins, und wechselt von Cookies zu Bearer-Tokens mit dem bearer Plugin. E-Mail und Passwort funktionieren über SDK direkt. Für Google und Apple holst du dir einen ID-Token über ein natives Plugin wie @capgo/capacitor-social-login und übergibst ihn authClient.signIn.social({ idToken }).

Dieser Guide zeigt das Server- und Client-Setup, die Token-Speicherung, Social Sign-in, Enterprise-Provider und die typischen Fehler, wenn man eine Better Auth Web-App in Capacitor migriert.

Was ändert sich, wenn Better Auth in Capacitor läuft?

Eine Capacitor App ist eine Web-App, die von einem lokalen Origin innerhalb einer nativen WebView ausgeliefert wird. Im Vergleich zu einer normalen Website gibt es drei Unterschiede:

Thema Auf einer Website In einer Capacitor App
Origin https://app.example.com capacitor://localhost (iOS), https://localhost (Android)
Auth-Server Oft dieselbe Domain, Cookies funktionieren einfach Andere Domain, Third-Party-Cookies werden von WKWebView blockiert
OAuth-Redirects Full-Page-Redirect ist okay Redirect verlässt deine App, Google blockiert WebViews

Die Lösungen: Vertraue den Capacitor Origins, nutze Bearer-Tokens und native Plugins für Social Login.

Schritt 1: Better Auth Server konfigurieren

Dieses Beispiel nutzt Hono, aber die Better Auth Konfiguration ist identisch für Express, Next.js Route Handler oder andere Runtimes.

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 ist für Apple wichtig. Tokens aus dem nativen iOS-Sheet nutzen deine Bundle ID als Audience, während Web- und Android-Tokens die Services ID nutzen. Better Auth verwendet dieses Feld, um beides zu akzeptieren.

Mounten Sie den Handler und erlauben Sie CORS für die App-Origins. Geben Sie den set-auth-token Header frei, sonst sieht der Client das Bearer-Token nie:

// 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;

Ausführen bunx @better-auth/cli migrate (oder generate für Ihr ORM), um die Tabellen zu erstellen.

Schritt 2: Auth-Client in der App erstellen

bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync

Speichern Sie den Session-Token im Keychain oder Keystore und halten Sie eine Kopie im Speicher, damit der Client ihn synchron lesen kann:

// 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('');
}

Rufe await loadSessionToken() bevor Ihre App rendert, dann authClient.getSession() um den User wiederherzustellen:

import { authClient, loadSessionToken } from './auth-client';

await loadSessionToken();
const { data: session } = await authClient.getSession();
if (session) {
  console.log('Signed in as', session.user.email);
}

Wenn Sie React, Vue, Svelte oder Solid nutzen, importieren Sie createAuthClient aus dem passenden Subpfad (better-auth/react, better-auth/vue, …), um den useSession Hook. Der fetchOptions bleiben gleich. Die setData und getData Methoden sind nur nativ. Im Web auf Cookies oder sessionStorage mit Capacitor.isNativePlatform().

Schritt 3: E-Mail und Passwort

Kein Plugin erforderlich. Das sind einfache HTTP-Calls.

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();
}

Für die E-Mail-Verifizierung und das Zurücksetzen des Passworts sendet Better Auth Links, die eine URL öffnen. Verweise callbackURL auf eine Seite deiner Domain, die ebenfalls als iOS Universal Link und Android App Link konfiguriert ist, damit der Link auf Handys die App und ansonsten die Website öffnet.

Schritt 4: Natives Google Sign-in

Konfiguriere Google wie in So meldest du dich mit Google über Capacitor an, dann:

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 verifiziert den Token gegen Googles Keys und die konfigurierte Client-ID, erstellt oder verknüpft den User und gibt eine Session zurück. Der Bearer Token kommt im set-auth-token Header an und dein onSuccess Hook speichert ihn.

Schritt 5: Native Apple Sign-in

Konfiguriere Apple wie in So meldest du dich mit Apple über Capacitor anbeschrieben. Generiere eine Nonce, übergib sie an Apple und sende denselben Wert an 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 übermittelt den Namen des Users nur bei der ersten Autorisierung. Wenn du ihn im User-Record brauchst, aktualisiere das Profil direkt nach dem ersten Sign-in mit result.profile.givenName und familyName.

Facebook folgt demselben Muster. Auf iOS mit Limited Login erhältst du einen idToken, an anderer Stelle übergibst du den Access Token. Der Better Auth Integration-Seite enthält das vollständige Facebook-Beispiel.

Schritt 6: Okta, Auth0, Entra ID und andere OIDC-Provider

Das Better Auth genericOAuth Plugin liefert Helfer für Auth0, Okta, Keycloak und Microsoft Entra ID sowie manuelle Konfigurationen für jeden OIDC-Server. Beim Web-Build authClient.signIn.oauth2({ providerId }) funktioniert wie auf jeder anderen Website.

Auf iOS und Android benötigt der Flow den System-Browser, da die Login-Seite des Providers nicht innerhalb deines WebViews geladen werden darf. Das bewährte Pattern:

  1. Die App öffnet deine Better Auth Sign-in URL im System-Browser (zum Beispiel mit SocialLogin.openSecureWindow oder einem In-App-Browser-Plugin).
  2. Better Auth führt den OAuth-Dance mit dem Provider aus und erstellt die Session auf dem Server.
  3. Deine Callback-Seite wandelt diese Session in einen kurzlebigen One-Time-Token um (Better Auth bietet einen oneTimeToken Plugin dafür) und leitet zu einem Deep Link wie etwa com.example.app://auth?token=....
  4. Die App verifiziert den One-Time-Token mit dem Auth-Client und erhält eine normale Bearer-Session.

Das sind mehr bewegliche Teile als beim ID-Token-Handoff. Wenn du nur Enterprise-SSO brauchst und die User-Datenbank von Better Auth nicht benötigst, ist das direkte Login beim Provider einfacher, siehe die Okta, Auth0 und Entra ID Guides.

Schritt 7: Schütze dein API mit der Session

Dein eigener API kann die Session über den Bearer-Token mit dem Server-API von Better Auth auslesen:

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));
});

Das Bearer-Plugin ermöglicht getSession akzeptieren Authorization: Bearer <token> genau so, wie es das Cookie akzeptiert.

Fehlerbehebung

Problem Ursache Lösung
Invalid origin oder 403 beim Sign-in Capacitor Origin wird nicht als vertrauenswürdig eingestuft Füge capacitor://localhost und https://localhost zu trustedOrigins
CORS-Fehler in der WebView-Konsole Server-CORS listet die App-Origins nicht auf Füge sie deiner CORS-Middleware hinzu, erlaube Authorization
Login erfolgreich, aber getSession gibt zurück null Sich auf Cookies über Domains hinweg verlassen Nutze das Bearer-Plugin und sende den Token
set-auth-token ist immer null Header nicht freigegeben Füge exposeHeaders: ['set-auth-token'] zu CORS hinzu
Google: disallowed_useragent OAuth-Seite im WebView geladen Nutze natives Sign-in und das idToken Handoff
Google ID Token abgelehnt Audience stimmt nicht mit der konfigurierten Client ID überein Token dekodieren mit SocialLogin.decodeIdToken und vergleichen aud mit clientId
Apple ID Token auf iOS abgelehnt Audience ist die Bundle ID Setzen Sie appBundleIdentifier im Apple Provider
Apple Nonce-Fehler Unterschiedliche Nonces an Apple und Better Auth gesendet Einmal generieren, mehrfach verwenden
Session geht nach App-Neustart verloren Token nur im Arbeitsspeicher gespeichert Persistieren mit setData und vor dem ersten Request laden

Deployment der mobilen Seite

Auth-Änderungen sind selten nativ. Trusted Origins und Provider sind Server-Konfiguration, und der Client code ist TypeScript, daher können die meisten Fixes via Capgo Live Updates statt über ein Store-Release verteilt werden. Ein neues Binary ist nur nötig, wenn du ein natives Plugin hinzufügst oder URL-Schemes änderst.

Live-Updates für Capacitor-Apps

Wenn ein Bug im Web-Layer live ist, liefere den Fix über Capgo aus, statt Tage auf die App-Store-Freigabe zu warten. Nutzer bekommen das Update im Hintergrund, während native Änderungen im normalen Review-Prozess bleiben.

Neues aus unserem Blog

Capgo gibt dir die besten Insights, um eine wirklich professionelle Mobile App zu erstellen.