Untuk menggunakan Better Auth di aplikasi Capacitor, instal better-auth client di web code Anda, arahkan ke server Better Auth Anda, tambahkan Capacitor origins (capacitor://localhost di iOS, https://localhost di Android) untuk trustedOrigins, dan beralih dari cookie ke bearer token dengan bearer plugin. Email dan password tetap berfungsi lewat SDK apa adanya. Untuk Google dan Apple, dapatkan ID token dengan native plugin seperti @capgo/capacitor-social-login dan berikan ke authClient.signIn.social({ idToken }).
Panduan ini membahas setup server dan client, penyimpanan token, social sign-in, enterprise provider, serta error yang sering muncul saat memindahkan web app Better Auth ke Capacitor 8.
Apa yang berubah saat Better Auth berjalan di dalam Capacitor
Aplikasi Capacitor adalah web app yang dijalankan dari local origin di dalam native WebView. Dibandingkan website biasa, ada tiga hal yang berbeda:
| Topik | Di website | Di aplikasi Capacitor |
|---|---|---|
| Asal | https://app.example.com |
capacitor://localhost (iOS), https://localhost (Android) |
| Server Auth | Sering kali masih satu site, cookies langsung jalan | Domain berbeda, third-party cookies diblokir oleh WKWebView |
| Redirect OAuth | Full-page redirect tidak masalah | Redirect keluar dari bundled app, Google memblokir WebView |
Solusinya: percayai origin Capacitor, gunakan bearer token, dan pakai plugin native untuk social login.
Langkah 1: Konfigurasi server Better Auth
Contoh ini menggunakan Hono, tetapi konfigurasi Better Auth tetap sama untuk Express, route handler Next.js, atau runtime lainnya.
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 penting bagi Apple. Token dari sheet iOS native membawa bundle ID sebagai audience, sedangkan token web dan Android membawa Services ID. Better Auth memakai field ini untuk menerima keduanya.
Mount handler dan izinkan CORS dari origin aplikasi. Ekspos set-auth-token header, jika tidak klien tidak akan pernah melihat 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;
Jalankan bunx @better-auth/cli migrate (atau generate untuk ORM Anda) untuk membuat tabel.
Langkah 2: Buat auth client di aplikasi
bun add better-auth @capgo/capacitor-native-biometric
bunx cap sync
Simpan session token di Keychain atau Keystore, dan simpan salinannya di memori agar klien dapat membacanya secara sinkron:
// 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('');
}
Panggil await loadSessionToken() sebelum aplikasi Anda dirender, kemudian authClient.getSession() untuk memulihkan pengguna:
import { authClient, loadSessionToken } from './auth-client';
await loadSessionToken();
const { data: session } = await authClient.getSession();
if (session) {
console.log('Signed in as', session.user.email);
}
Jika Anda menggunakan React, Vue, Svelte, atau Solid, import createAuthClient dari subpath yang sesuai (better-auth/react, better-auth/vue, …) untuk mendapatkan useSession hook. fetchOptions tetap sama. The setData dan getData metode ini hanya untuk native. Di web, gunakan cookie atau sessionStorage menggunakan Capacitor.isNativePlatform().
Langkah 3: Email dan password
Tidak perlu plugin. Ini hanya panggilan HTTP biasa.
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();
}
Untuk verifikasi email dan reset password, Better Auth mengirimkan link yang membuka URL. Arahkan callbackURL ke halaman di domain Anda yang juga dikonfigurasi sebagai iOS Universal Link dan Android App Link, agar link tersebut membuka aplikasi di ponsel dan website di perangkat lain.
Langkah 4: Login Google native
Konfigurasikan Google seperti dijelaskan di Cara login dengan Google menggunakan Capacitor, lalu:
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 memverifikasi token terhadap kunci Google dan client ID yang dikonfigurasi, membuat atau menautkan user, lalu mengembalikan session. Bearer token masuk melalui set-auth-token header dan milik Anda onSuccess hook Anda menyimpannya.
Langkah 5: Native Apple sign-in
Konfigurasikan Apple seperti dijelaskan di Cara sign in dengan Apple menggunakan Capacitor. Buat nonce, teruskan ke Apple, dan kirim nilai yang sama ke 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 hanya membagikan nama user pada otorisasi pertama. Jika ingin nama itu ada di record user, update profil tepat setelah sign-in pertama dengan result.profile.givenName dan familyName.
Facebook mengikuti pola yang sama. Di iOS dengan Limited Login, Anda mendapatkan idToken, di tempat lain Anda meneruskan access token. The Halaman integrasi Better Auth berisi contoh lengkap Facebook.
Langkah 6: Okta, Auth0, Entra ID, dan provider OIDC lainnya
Better Auth memiliki genericOAuth plugin yang menyertakan helper untuk Auth0, Okta, Keycloak, dan Microsoft Entra ID, plus konfigurasi manual untuk server OIDC apa pun. Pada build web, authClient.signIn.oauth2({ providerId }) berjalan seperti di situs mana pun.
Di iOS dan Android, alur ini butuh browser sistem, karena halaman login provider tidak boleh dimuat di dalam WebView Anda. Pola yang berhasil:
- Aplikasi membuka URL sign-in Better Auth Anda di browser sistem (misalnya dengan
SocialLogin.openSecureWindowatau plugin in-app browser). - Better Auth menjalankan proses OAuth dengan provider dan membuat session di server.
- Halaman callback Anda mengubah session itu menjadi one-time token berumur pendek (Better Auth punya
oneTimeTokenplugin untuk ini) dan mengalihkan ke deep link seperticom.example.app://auth?token=.... - Aplikasi memverifikasi token sekali pakai dengan auth client dan menerima sesi bearer biasa.
Ini melibatkan lebih banyak komponen daripada handoff ID token. Jika Anda hanya membutuhkan enterprise SSO dan tidak memerlukan database pengguna Better Auth, masuk langsung ke provider lebih sederhana, lihat Okta, Auth0 dan Entra ID panduan.
Langkah 7: Lindungi API Anda dengan sesi
API Anda sendiri dapat membaca sesi dari bearer token melalui API server 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));
});
plugin bearer membuat getSession menerima Authorization: Bearer <token> dengan cara yang sama seperti menerima cookie.
Pemecahan Masalah
| Masalah | Penyebab | Perbaikan |
|---|---|---|
Invalid origin atau 403 saat sign-in |
origin Capacitor tidak dipercaya | Tambah capacitor://localhost dan https://localhost ke trustedOrigins |
| Error CORS di konsol WebView | CORS server tidak mencantumkan origin aplikasi | Tambahkan ke middleware CORS Anda, izinkan Authorization |
Sign-in berhasil tetapi getSession mengembalikan null |
Mengandalkan cookie lintas domain | Gunakan plugin bearer dan kirim token |
set-auth-token selalu null |
Header tidak terekspos | Tambah exposeHeaders: ['set-auth-token'] ke CORS |
Google: disallowed_useragent |
Halaman OAuth dimuat di WebView | Gunakan native sign-in dan idToken handoff |
| ID token Google ditolak | Audience tidak cocok dengan client ID yang dikonfigurasi | Decode token dengan SocialLogin.decodeIdToken dan bandingkan aud dengan clientId |
| ID token Apple ditolak di iOS | Audience adalah bundle ID | Atur appBundleIdentifier di provider Apple |
| Error nonce Apple | Nonce berbeda dikirim ke Apple dan Better Auth | Generate sekali, gunakan nilai yang sama |
| Sesi hilang setelah aplikasi di-restart | Token hanya disimpan di memori | Simpan dengan setData dan muat sebelum request pertama |
Deploy sisi mobile
Perubahan auth jarang bersifat native. Trusted origins dan provider diatur di server, dan client code adalah TypeScript, jadi sebagian besar perbaikan bisa dikirim via Live updates Capgo daripada merilis ulang di store. Anda hanya butuh binary baru saat menambahkan native plugin atau mengubah URL scheme.
Panduan terkait
- Cara menambah autentikasi ke aplikasi Capacitor membandingkan Better Auth dengan Firebase, Supabase, Neon Auth (managed Better Auth) dan identity provider hosted.
- Autentikasi biometrik di aplikasi Capacitor untuk membuka sesi tersimpan dengan Face ID atau sidik jari.
- plugin Social Login untuk alur Google, Apple, dan Facebook native.