Dart/Flutter Better Auth

A Dart/Flutter client for Better Auth — sign-in, sign-up, social/OAuth, session management, and every first-party plugin (2FA, organizations, passkeys, API keys, admin, phone, email-OTP, JWT, one-time-token, anonymous).

  • Encrypted session storage by default (Keychain/Keystore).
  • Reactive auth state (onAuthChange) with refresh on app-resume and network-reconnect.
  • Typed request/response models for every endpoint, validated against the server's OpenAPI schema.

Contents

Install

flutter pub add flutter_better_auth

Quick start

import 'package:flutter/material.dart';
import 'package:flutter_better_auth/flutter_better_auth.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterBetterAuth.initialize(
    url: 'https://your-server.com/api/auth', // Better Auth base URL
    scheme: 'myapp',                         // deep-link scheme (required for native social)
  );
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return BetterAuthProvider(
      child: MaterialApp(home: const HomePage()),
    );
  }
}

Access the client anywhere:

// From the widget tree:
BetterAuthConsumer(
  builder: (context, client) => /* use client */,
);

// Or directly:
final client = FlutterBetterAuth.client;

initialize() options:

Param Default Purpose
url Better Auth base URL (…/api/auth).
scheme null Deep-link scheme. Required for native social sign-in — sent as expo-origin.
store secure storage Custom StorageInterface (see Storage).
dio new Dio Provide your own Dio instance.
refreshSessionOnAppResume true Refresh session when the app returns to foreground.
refreshSessionOnReconnect true Refresh session when the device regains connectivity.

Server setup

Install the matching plugins on your Better Auth server and trust your app's scheme:

import { betterAuth } from "better-auth";
import { expo } from "@better-auth/expo";

export const auth = betterAuth({
  trustedOrigins: ["myapp://"],         // your app scheme
  plugins: [
    expo(),                              // required for native social sign-in
    // twoFactor(), organization(), admin(), passkey(), jwt(),
    // phoneNumber(), emailOTP(), apiKey(), anonymous(), oneTimeToken(), ...
  ],
  emailAndPassword: { enabled: true },
  socialProviders: { /* google, github, … */ },
});

Each Flutter plugin below requires its server-side counterpart to be enabled.

Working with Result

Every call returns Result<T> (a sealed type). Use the data / error getters:

final result = await client.signIn.email(
  email: 'test@mail.com',
  password: '12345678',
);

if (result.data != null) {
  print(result.data); // typed response (e.g. SignInEmailResponse)
} else {
  print('${result.error?.code}: ${result.error?.message}');
}

Or pattern-match:

switch (result) {
  case Success(:final data): /* … */;
  case Failure(:final error): /* … */;
}

Session & auth state

final client = FlutterBetterAuth.client;

// Current session (user + session):
final res = await client.getSession();
final user = res.data?.user;

// Reactive auth state — emits User? on sign-in/out, resume, reconnect:
StreamBuilder<User?>(
  stream: client.onAuthChange,
  builder: (context, snap) => Text(snap.data == null ? 'Signed out' : 'Hi ${snap.data!.name}'),
);

// Force a refresh:
await FlutterBetterAuth.refreshSession();

// Sign out:
await client.signOut();

// Sessions management:
await client.listSessions();
await client.revokeSession(token: '<session-token>');
await client.revokeOtherSessions();
await client.revokeSessions();

Email & password

// Sign up
await client.signUp.email(
  name: 'Test User',
  email: 'test@mail.com',
  password: '12345678',
);

// Sign in
await client.signIn.email(
  email: 'test@mail.com',
  password: '12345678',
  rememberMe: true, // optional (bool)
);

// Update profile
await client.updateUser(name: 'New Name', image: 'https://…/avatar.png');

// Change password
await client.changePassword(
  currentPassword: '12345678',
  newPassword: '87654321',
  revokeOtherSessions: true,
);

// Verify the current password (e.g. before a sensitive action)
await client.verifyPassword(password: '12345678');

// Forgot / reset password
await client.forgotPassword(email: 'test@mail.com');
await client.resetPassword(newPassword: '87654321', token: '<token-from-email>');

// Email verification
await client.sendVerificationEmail(email: 'test@mail.com');
await client.verifyEmail(token: '<token>');

// Change email
await client.changeEmail(newEmail: 'new@mail.com');

// Delete account
await client.deleteUser(password: '12345678');

Username

Requires the username() server plugin.

await client.signIn.username(
  username: 'testuser',
  password: '12345678',
  rememberMe: true,
);

// Check availability before sign-up
final res = await client.signIn.isUsernameAvailable(username: 'testuser');
final free = res.data?.available;

Anonymous

Requires the anonymous() server plugin.

import 'package:flutter_better_auth/plugins/anonymous/anonymous_plugin.dart';

// Sign in anonymously (available without the extra import):
await client.signIn.anonymous();

// Delete the anonymous user (needs the import above):
await client.anonymous.deleteAnonymousUser();

Linking an anonymous account to a later sign-in (onLinkAccount) is configured on the server.

Social / OAuth

Two flows: redirect (opens a browser, e.g. GitHub) and idToken (native, e.g. Google). Both require the server expo() plugin and your scheme in trustedOrigins.

Redirect flow (GitHub, etc.)

await client.signIn.social(
  provider: 'github',
  callbackUrlScheme: 'myapp', // your deep-link scheme
  scopes: ['read:user'],      // optional (List<String>)
);

The client opens /expo-authorization-proxy, runs the OAuth round-trip in a secure web view, and persists the session cookie returned via the myapp:// deep link.

On web, signIn.social(...) instead does a full-page browser redirect to the provider and returns to callbackURL (defaults to the current app origin), with the browser handling the session cookie. The server's redirect/OAuth callback URLs must point at the server's own URL, and the OAuth provider's allowed callback must include <server>/api/auth/callback/<provider>.

Android — register the callback activity in android/app/src/main/AndroidManifest.xml:

<activity
  android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
  android:exported="true"
  android:taskAffinity="">
  <intent-filter android:label="flutter_web_auth_2">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="myapp" />
  </intent-filter>
</activity>

idToken flow (Google native)

Use google_sign_in to obtain an ID token, then:

import 'package:google_sign_in/google_sign_in.dart';

await GoogleSignIn.instance.initialize(
  serverClientId: '<google-WEB-oauth-client-id>', // must match server's google clientId
);
final account = await GoogleSignIn.instance.authenticate();
final idToken = account.authentication.idToken!;

await client.signIn.social(
  provider: 'google',
  idToken: SocialIdTokenBody(token: idToken),
);

Google Cloud setup (same project as the web client):

  • Web OAuth client → use its id as serverClientId and the server's socialProviders.google.clientId.
  • Android OAuth client → your app package + signing SHA-1 (no API exists to create this; do it in the console/Firebase).
  • iOS OAuth client → put its reversed id as a CFBundleURLTypes scheme in Info.plist.
  • Configure the OAuth consent screen (a fresh project without one throws "Developer console is not set up correctly").

Account linking

// Link returns the authorization `url`; drive the redirect yourself, or use
// linkAndRedirect (web: full-page redirect, native: web-auth, like signIn.social):
await client.social.linkAndRedirect(provider: 'github', scopes: ['repo']);
await client.social.link(provider: 'github', callbackURL: 'myapp://', scopes: ['repo']);
await client.social.unlink(providerId: 'github');
await client.social.listAccounts();
await client.social.refreshToken(providerId: 'google');
await client.social.getAccessToken(providerId: 'google');

Phone number

Requires the phoneNumber() server plugin.

import 'package:flutter_better_auth/plugins/phone/phone_plugin.dart';

await client.phone.sendOtp(phoneNumber: '+15555550123');
await client.phone.verify(phoneNumber: '+15555550123', code: '123456');
await client.phone.signIn(phoneNumber: '+15555550123', password: '12345678', rememberMe: true);

// Password reset by phone
await client.phone.requestPasswordResetOTP(phoneNumber: '+15555550123');
await client.phone.restPassword(otp: '123456', phoneNumber: '+15555550123', newPassword: '87654321');

Email OTP

Requires the emailOTP() server plugin.

import 'package:flutter_better_auth/plugins/email_otp/email_otp_plugin.dart';

await client.emailOtp.sendVerification(email: 'test@mail.com', type: 'sign-in');
await client.emailOtp.signIn(email: 'test@mail.com', otp: '123456');
await client.emailOtp.verifyEmail(email: 'test@mail.com', otp: '123456');

// Password reset by OTP
await client.emailOtp.forgotPassword(email: 'test@mail.com');
await client.emailOtp.resetPassword(email: 'test@mail.com', otp: '123456', password: '87654321');

// Check an OTP without consuming it; change email
await client.emailOtp.checkVerificationOtp(email: 'test@mail.com', type: 'sign-in', otp: '123456');
await client.emailOtp.requestEmailChange(newEmail: 'new@mail.com');
await client.emailOtp.changeEmail(newEmail: 'new@mail.com', otp: '123456');

type is one of 'sign-in', 'email-verification', 'forget-password'.

Requires the magicLink() server plugin. The server delivers the link; the user opens it, your app handles the deep link, then you verify the token.

import 'package:flutter_better_auth/plugins/magic_link/magic_link_plugin.dart';

// Request a magic link by email
await client.magicLink.signIn(email: 'test@mail.com', callbackURL: 'myapp://');

// After the user opens the link, exchange the token for a session
await client.magicLink.verify(token: '<token-from-deep-link>');

Multi-Session

Requires the multiSession() server plugin — multiple signed-in accounts on one device.

import 'package:flutter_better_auth/plugins/multi_session/multi_session_plugin.dart';

final sessions = await client.multiSession.listDeviceSessions();
await client.multiSession.setActive(sessionToken: '<token>');
await client.multiSession.revoke(sessionToken: '<token>');

Two-Factor (2FA)

Requires the twoFactor() server plugin.

import 'package:flutter_better_auth/plugins/two_factor/two_factor_plugin.dart';

// Enrollment
await client.twoFactor.enable(password: '12345678');     // returns TOTP secret/URI + backup codes
await client.twoFactor.getTotpUri(password: '12345678');  // for the QR code
await client.twoFactor.disable(password: '12345678');

// Verify (during sign-in challenge)
await client.twoFactor.verifyTotp(code: '123456', trustDevice: true);
await client.twoFactor.sendOtp();                          // OTP via email/SMS
await client.twoFactor.verifyOtp(code: '123456');
await client.twoFactor.verifyBackupCode(code: 'abcd-efgh');

// Backup codes
await client.twoFactor.generateBackupCodes(password: '12345678');
await client.twoFactor.viewBackupCodes(userId: '<user-id>');

Passkey (WebAuthn)

Requires the @better-auth/passkey server plugin. This client exposes the HTTP surface only — the actual credential ceremony (FIDO2/WebAuthn) is done by an app-side package such as passkeys.

import 'package:flutter_better_auth/plugins/passkey/passkey_plugin.dart';

// 1. Get options from the server
final opts = await client.passkey.generateRegistrationOptions(/* … */);
// 2. Run the device ceremony with your WebAuthn package → credential JSON
// 3. Send it back
await client.passkey.verifyRegistration(/* body: { response, name } */);

// Sign-in
await client.passkey.generateAuthenticationOptions();
await client.passkey.verifyAuthentication(/* body: { response } */);

// Manage
await client.passkey.listUserPasskeys();
await client.passkey.updatePasskey(id: '<id>', name: 'My phone');
await client.passkey.deletePasskey(id: '<id>');

Admin

Requires the admin() server plugin and an admin session.

import 'package:flutter_better_auth/plugins/admin/admin_plugin.dart';

await client.admin.listUsers(limit: 20, offset: 0, searchValue: 'john');
await client.admin.getUser(id: '<user-id>');
await client.admin.createUser(email: 'new@mail.com', password: '12345678', name: 'New', role: 'user');
await client.admin.setRole(userId: '<id>', role: 'admin');
await client.admin.setUserPassword(userId: '<id>', newPassword: '87654321');
await client.admin.hasPermission(permissions: {'user': ['create']});

await client.admin.banUser(userId: '<id>', banReason: 'spam', banExpiresIn: 86400);
await client.admin.unbanUser(userId: '<id>');

await client.admin.listUserSessions(userId: '<id>');
await client.admin.revokeUserSessions(userId: '<id>');

await client.admin.impersonateUser(userId: '<id>');
await client.admin.stopImpersonating();
await client.admin.removeUser(userId: '<id>');

API Key

Requires the apiKey() server plugin.

import 'package:flutter_better_auth/plugins/api_key/api_key_plugin.dart';

final created = await client.apiKey.create(name: 'my-key', expiresIn: 3600);
await client.apiKey.list();
await client.apiKey.fetch(id: '<key-id>');
await client.apiKey.update(keyId: '<key-id>', enabled: false);
await client.apiKey.verify(key: '<plaintext-key>');
await client.apiKey.delete(keyId: '<key-id>');
await client.apiKey.deleteAllExpired();

Organization

Requires the organization() server plugin.

import 'package:flutter_better_auth/plugins/organization/organization_plugin.dart';

await client.organization.checkSlug(slug: 'acme');
await client.organization.create(name: 'Acme', slug: 'acme');
await client.organization.listOrganizations();
await client.organization.getFullOrganization(organizationId: '<org-id>');
await client.organization.leave(organizationId: '<org-id>');

// Members & invitations
await client.organization.listMembers(limit: 20);
await client.organization.inviteMember(email: 'member@mail.com', role: 'member', organizationId: '<org-id>');

getSession() populates session.activeOrganizationId once an active org is set on the server, and session.activeTeamId when teams are enabled.

For server-side additional schema fields, use the *Raw variants with a JSON map: createRaw(...), inviteMemberRaw(...), addMemberRaw(...), and listMembersRaw({...}) for non-string list-member filters.

JWT

Requires the jwt() server plugin.

import 'package:flutter_better_auth/plugins/jwt/jwt_plugin.dart';

final token = await client.jwt.token();  // signed JWT for the current session
final jwks = await client.jwt.jwks();     // public JWKS

One-Time Token

Requires the oneTimeToken() server plugin.

import 'package:flutter_better_auth/plugins/one_time_token/one_time_token_plugin.dart';

final generated = await client.oneTimeToken.generate();    // GET /one-time-token/generate
final session = await client.oneTimeToken.verify(token: '<token>'); // POST /one-time-token/verify

Storage

By default, session cookies are stored encrypted on native platforms (flutter_secure_storage, chunked for the iOS Keychain). To customize, pass a StorageInterface:

// Built-in unencrypted Hive store (opt-in, native only — uses dart:io):
import 'package:flutter_better_auth/flutter_better_auth.dart';
import 'package:flutter_better_auth/core/storage/hive_storage.dart';

await HiveStorage.init(); // opens the Hive box first
await FlutterBetterAuth.initialize(
  url: '…',
  store: HiveStorage(), // or your own StorageInterface implementation
);

A custom store implements:

abstract class StorageInterface {
  Future<void> saveCookies(String host, List<Cookie> cookies);
  Future<List<Cookie>> loadCookies(String host);
}

Web

On web the browser owns cookies (JavaScript cannot set the Cookie header), so the client skips the cookie jar and instead enables withCredentials — the browser stores Better Auth's Set-Cookie and sends it automatically. Nothing to configure in the app. If your web app and server are different origins, the server must set the session cookie SameSite=None; Secure, send Access-Control-Allow-Credentials: true with a specific Access-Control-Allow-Origin (not *), and list your web origin in trustedOrigins.

Web + separated server (different origins)

When the web app and the Better Auth server are on different origins, the session cookie is third-party and modern browsers block it — so getSession() can return null right after a successful sign-in. There are two classes of flow:

Direct flows (email, username, phone, email-OTP, etc.) — solved by the bearer plugin. Enable bearer on the server; the client automatically captures the set-auth-token header on sign-in and sends Authorization: Bearer <token> on every request. No cookies, works across any origin, nothing to configure in the client.

Social / OAuth (redirect) flows — the OAuth round-trip happens via browser navigation, so the client can't capture a bearer token. Follow Better Auth's recommended cross-origin setup (cookies must reach the server):

  1. Subdomains of one root domain (recommended). Host app and server as app.example.com / api.example.com and enable cross-subdomain cookies on the server so the cookie is first-party for both:
    advanced: {
      crossSubDomainCookies: { enabled: true, domain: "example.com" },
    }
    
  2. Reverse proxy. Route /api/auth through the frontend's domain (Vercel/Next rewrites, nginx proxy_pass, etc.) so it's same-origin and the cookie is first-party.

Either way the server still needs CORS for credentials (Access-Control-Allow-Credentials: true, a specific Access-Control-Allow-Origin, not *), useSecureCookies in production, and your web origin in trustedOrigins.

Local dev with localhost + an ngrok/tunnel URL shares no root domain, so cross-subdomain cookies don't apply — use a reverse proxy for web social, or test social on native (which uses the deep-link flow).

Plugin imports

The default import gives you signIn, signUp, social, sessions, and account methods. Each plugin getter needs its file imported:

import 'package:flutter_better_auth/plugins/admin/admin_plugin.dart';            // client.admin
import 'package:flutter_better_auth/plugins/anonymous/anonymous_plugin.dart';    // client.anonymous
import 'package:flutter_better_auth/plugins/phone/phone_plugin.dart';            // client.phone
import 'package:flutter_better_auth/plugins/email_otp/email_otp_plugin.dart';    // client.emailOtp
import 'package:flutter_better_auth/plugins/jwt/jwt_plugin.dart';                // client.jwt
import 'package:flutter_better_auth/plugins/api_key/api_key_plugin.dart';        // client.apiKey
import 'package:flutter_better_auth/plugins/two_factor/two_factor_plugin.dart';  // client.twoFactor
import 'package:flutter_better_auth/plugins/organization/organization_plugin.dart'; // client.organization
import 'package:flutter_better_auth/plugins/passkey/passkey_plugin.dart';        // client.passkey
import 'package:flutter_better_auth/plugins/one_time_token/one_time_token_plugin.dart'; // client.oneTimeToken
import 'package:flutter_better_auth/plugins/magic_link/magic_link_plugin.dart';        // client.magicLink
import 'package:flutter_better_auth/plugins/multi_session/multi_session_plugin.dart';   // client.multiSession

Example app

The example/ directory is a tabbed test harness that exercises every plugin with live output, plus a Next.js Better Auth server under web/. See example/README.md for setup (including Google native sign-in).

Author

Tsiresy Milà

Libraries

core/api/adapter
core/api/bearer_interceptor
core/api/better_auth_client
core/api/default/sign_in/models/email/sign_in_email_response
core/api/default/sign_in/models/social/sign_in_social_response
core/api/default/sign_in/models/social/social_id_token_body
core/api/default/sign_in/models/username/username_available_response
core/api/default/sign_in/sign_in_better_auth
core/api/default/sign_in/sign_in_extension
core/api/default/sign_up/models/sign_up_response/sign_up_response
core/api/default/sign_up/sign_up_better_auth
core/api/default/sign_up/sign_up_extension
core/api/default/social/models/list_account/social_account_response
core/api/default/social/models/token/token_response
core/api/default/social/social_better_auth
core/api/default/social/social_extension
core/api/interceptor
core/api/models/common/change_email/change_email_response
core/api/models/common/sign_out/sign_out_response
core/api/models/common/user_wrapper/user_wrapper_response
core/api/models/common/verify_email/verify_email_response
core/api/models/result/better_error
core/api/models/result/result
core/api/models/result/result_extension
core/api/models/result/status_response
core/api/models/result/success_response
core/api/models/session/session_response
core/api/web_credentials
core/api/web_credentials_io
core/api/web_credentials_web
core/api/web_redirect
core/api/web_redirect_io
core/api/web_redirect_web
core/flutter_better_auth
core/models/account/account
core/models/organization/organization
core/models/session/session
core/models/two_factor/two_factor
core/models/user/user
core/models/verification/verification
core/storage/hive_storage
core/storage/memory_storage
core/storage/secure_storage
core/storage/storage
core/utils/logger
flutter_better_auth
plugins/admin/admin_better_auth
plugins/admin/admin_extension
plugins/admin/models/admin_models
plugins/anonymous/anonymous_better_auth
plugins/anonymous/anonymous_extension
plugins/anonymous/anonymous_plugin
plugins/api_key/api_key_better_auth
plugins/api_key/api_key_extension
plugins/api_key/api_key_plugin
plugins/api_key/models/api_key
plugins/api_key/models/api_key_model
plugins/api_key/models/api_keys_list_response
plugins/api_key/models/delete_expired_api_keys_response
plugins/api_key/models/verify_api_key_response
plugins/email_otp/email_otp_better_auth
plugins/email_otp/email_otp_extension
plugins/email_otp/email_otp_plugin
plugins/jwt/jwt_better_auth
plugins/jwt/jwt_extension
plugins/jwt/jwt_plugin
plugins/jwt/models/jwk_key/jwt_key
plugins/jwt/models/jwt_key_response/jwt_key_response
plugins/jwt/models/token_response/token_response
plugins/multi_session/multi_session_better_auth
plugins/multi_session/multi_session_extension
plugins/multi_session/multi_session_plugin
plugins/one_time_token/models/one_time_token_generate_response
plugins/one_time_token/models/one_time_token_models
plugins/one_time_token/one_time_token_better_auth
plugins/one_time_token/one_time_token_extension
plugins/one_time_token/one_time_token_plugin
plugins/organization/models/invitation_with_member_response
plugins/organization/models/organization_create_role_response
plugins/organization/models/organization_invitation
plugins/organization/models/organization_member
plugins/organization/models/organization_member_role_response
plugins/organization/models/organization_members_page
plugins/organization/models/organization_operation_success
plugins/organization/models/organization_payload
plugins/organization/models/organization_plain_message
plugins/organization/models/organization_role_record
plugins/organization/models/organization_slug_check_response
plugins/organization/models/organization_team
plugins/organization/models/organization_team_member
plugins/organization/models/organization_update_role_response
plugins/organization/models/wrapped_organization_member_response
plugins/organization/organization_better_auth
plugins/organization/organization_extension
plugins/organization/organization_plugin
plugins/organization/organization_retrofit
plugins/passkey/models/passkey_record
plugins/passkey/models/passkey_update_response
plugins/passkey/passkey_better_auth
plugins/passkey/passkey_extension
plugins/passkey/passkey_plugin
plugins/phone/models/send_otp/send_otp_response
plugins/phone/phone_better_auth
plugins/phone/phone_extension
plugins/phone/phone_plugin
plugins/two_factor/models/totp_uri_response
plugins/two_factor/models/two_factor_backup_codes_response
plugins/two_factor/models/two_factor_enable_response
plugins/two_factor/models/two_factor_responses
plugins/two_factor/models/two_factor_totp_uri_response
plugins/two_factor/two_factor_better_auth
plugins/two_factor/two_factor_extension
plugins/two_factor/two_factor_plugin
presentation/better_auth_consumer
presentation/better_auth_inherit
presentation/better_auth_provider