Flutter InstantDB
Authentication

User Management

Authentication and user management with Flutter InstantDB

InstantDB provides comprehensive user authentication with email/password, magic links, magic codes, and session management. All authentication methods integrate seamlessly with real-time sync and presence features.

Getting Started with Auth

Initialize with Authentication

final db = await InstantDB.init(
  appId: 'your-app-id',
  config: const InstantConfig(
    syncEnabled: true,
  ),
);

// Listen to authentication state changes
db.auth.onAuthStateChange.listen((user) {
  if (user != null) {
    print('User signed in: ${user.email}');
  } else {
    print('User signed out');
  }
});

Check Authentication Status

// One-time check
final currentUser = db.getAuth();

// Reactive updates
final authSignal = db.subscribeAuth();

// Use in widgets
Watch((context) {
  final user = db.subscribeAuth().value;
  return user != null 
    ? WelcomeScreen(user: user)
    : LoginScreen();
});

Authentication Methods

Magic Codes

One-time password (OTP) authentication:

class MagicCodeAuth extends StatefulWidget {
  @override
  State<MagicCodeAuth> createState() => _MagicCodeAuthState();
}

class _MagicCodeAuthState extends State<MagicCodeAuth> {
  final _emailController = TextEditingController();
  final _codeController = TextEditingController();
  bool _isLoading = false;
  bool _codeSent = false;
  String? _errorMessage;

  Future<void> _sendMagicCode() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final db = InstantProvider.of(context);
      await db.auth.sendMagicCode(_emailController.text.trim());
      
      setState(() {
        _codeSent = true;
      });
    } on InstantException catch (e) {
      setState(() {
        _errorMessage = e.message;
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  Future<void> _verifyCode() async {
    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final db = InstantProvider.of(context);
      final user = await db.auth.verifyMagicCode(
        email: _emailController.text.trim(),
        code: _codeController.text.trim(),
      );

      print('User authenticated: ${user.email}');
      // Navigation handled by AuthBuilder
    } on InstantException catch (e) {
      setState(() {
        _errorMessage = e.message;
      });
    } finally {
      setState(() {
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          controller: _emailController,
          decoration: const InputDecoration(
            labelText: 'Email Address',
            keyboardType: TextInputType.emailAddress,
          ),
          enabled: !_codeSent,
        ),
        const SizedBox(height: 16),

        if (_codeSent) ...[
          TextField(
            controller: _codeController,
            decoration: const InputDecoration(
              labelText: 'Verification Code',
              hintText: 'Enter 6-digit code',
            ),
            keyboardType: TextInputType.number,
            maxLength: 6,
          ),
          const SizedBox(height: 16),
        ],

        if (_errorMessage != null)
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.red.shade100,
              borderRadius: BorderRadius.circular(8),
            ),
            child: Text(
              _errorMessage!,
              style: TextStyle(color: Colors.red.shade700),
            ),
          ),
        const SizedBox(height: 16),

        SizedBox(
          width: double.infinity,
          child: ElevatedButton(
            onPressed: _isLoading ? null : (_codeSent ? _verifyCode : _sendMagicCode),
            child: _isLoading
                ? const CircularProgressIndicator()
                : Text(_codeSent ? 'Verify Code' : 'Send Code'),
          ),
        ),

        if (_codeSent) ...[
          const SizedBox(height: 16),
          OutlinedButton(
            onPressed: () {
              setState(() {
                _codeSent = false;
                _codeController.clear();
              });
            },
            child: const Text('Use Different Email'),
          ),
        ],
      ],
    );
  }
}

Complete Authentication Flow

Combine all authentication methods in a unified experience:

class AuthFlow extends StatefulWidget {
  @override
  State<AuthFlow> createState() => _AuthFlowState();
}

class _AuthFlowState extends State<AuthFlow> {
  AuthMethod _currentMethod = AuthMethod.emailPassword;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Sign In'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          children: [
            // Method selector
            SegmentedButton<AuthMethod>(
              segments: const [
                ButtonSegment(
                  value: AuthMethod.emailPassword,
                  label: Text('Email/Password'),
                  icon: Icon(Icons.password),
                ),
                ButtonSegment(
                  value: AuthMethod.magicLink,
                  label: Text('Magic Link'),
                  icon: Icon(Icons.link),
                ),
                ButtonSegment(
                  value: AuthMethod.magicCode,
                  label: Text('Magic Code'),
                  icon: Icon(Icons.pin),
                ),
              ],
              selected: {_currentMethod},
              onSelectionChanged: (selection) {
                setState(() {
                  _currentMethod = selection.first;
                });
              },
            ),
            const SizedBox(height: 32),

            // Authentication method
            Expanded(
              child: switch (_currentMethod) {
                AuthMethod.emailPassword => EmailPasswordAuth(),
                AuthMethod.magicLink => MagicLinkAuth(),
                AuthMethod.magicCode => MagicCodeAuth(),
              },
            ),
          ],
        ),
      ),
    );
  }
}

enum AuthMethod {
  emailPassword,
  magicLink,
  magicCode,
}

Reactive Authentication Widget

Use the AuthBuilder widget for reactive authentication UI:

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return InstantProvider(
      db: db,
      child: AuthBuilder(
        builder: (context, user) {
          if (user != null) {
            // User is authenticated
            return MainApp(user: user);
          } else {
            // User needs to sign in
            return AuthFlow();
          }
        },
        loadingBuilder: (context) {
          return const Scaffold(
            body: Center(
              child: CircularProgressIndicator(),
            ),
          );
        },
      ),
    );
  }
}

class MainApp extends StatelessWidget {
  final AuthUser user;

  const MainApp({super.key, required this.user});

  @override
  Widget build(BuildContext context) {
    final db = InstantProvider.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('Welcome, ${user.email}'),
        actions: [
          PopupMenuButton(
            itemBuilder: (context) => [
              PopupMenuItem(
                child: const Text('Profile'),
                onTap: () => _showProfile(context),
              ),
              PopupMenuItem(
                child: const Text('Sign Out'),
                onTap: () => db.auth.signOut(),
              ),
            ],
          ),
        ],
      ),
      body: YourAppContent(),
    );
  }

  void _showProfile(BuildContext context) {
    showDialog(
      context: context,
      builder: (context) => Dialog(
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: UserProfile(user: user),
        ),
      ),
    );
  }
}

OAuth Sign-In

InstantDB supports OAuth providers (Google, Apple, GitHub, LinkedIn, Clerk, Firebase) through either the redirect flow or provider-specific ID token helpers.

Redirect Flow

Build an authorization URL, open it, and exchange the returned code for a session. PKCE (S256) is enabled by default.

// 1. Build the authorization URL
final flow = db.auth.createAuthorizationUrl(
  clientName: 'google',          // OAuth client name from the InstantDB dashboard
  redirectUri: 'myapp://oauth',  // must match an allowed redirect for the client
  scopes: ['email', 'profile'],  // optional
);

// 2. Open flow.url (url_launcher, flutter_web_auth_2, in-app webview, ...)
//    On redirect you receive ?code=...

// 3. Exchange the code for a session
await db.auth.exchangeCodeForToken(
  code: code,
  codeVerifier: flow.codeVerifier,
);

createAuthorizationUrl returns an OAuthFlow:

class OAuthFlow {
  final String url;
  final String? codeVerifier; // pass back to exchangeCodeForToken
  final String? state;
}

Provider ID Token Helpers

If you already have an ID token from a provider's native SDK (e.g. google_sign_in, sign_in_with_apple), pass it directly. Each helper wraps signInWithIdToken and takes the dashboard clientName.

await db.auth.signInWithGoogle(idToken: googleIdToken);   // clientName defaults to 'google'
await db.auth.signInWithApple(idToken: appleIdToken);     // clientName defaults to 'apple'
await db.auth.signInWithClerk(idToken: clerkToken, clientName: 'clerk');
await db.auth.signInWithFirebase(idToken: firebaseToken, clientName: 'firebase');

Google and Apple also accept an optional nonce.

OAuthButton Widget

OAuthButton is a drop-in sign-in button. It builds the authorization URL and hands the OAuthFlow to your onLaunch callback — your app opens the URL (it does not bundle url_launcher) and calls exchangeCodeForToken on return.

OAuthButton(
  provider: OAuthProvider.google,
  clientName: 'google',
  redirectUri: 'myapp://oauth',
  onLaunch: (flow) async {
    final result = await FlutterWebAuth2.authenticate(
      url: flow.url,
      callbackUrlScheme: 'myapp',
    );
    final code = Uri.parse(result).queryParameters['code']!;
    await db.auth.exchangeCodeForToken(
      code: code,
      codeVerifier: flow.codeVerifier,
    );
  },
)

OAuthProvider supplies a default label, color, and icon for google, apple, github, linkedin, clerk, and firebase. Override with label, color, or icon, and pass scopes or usePKCE: false as needed.

Best Practices

1. Handle Authentication States

Always provide loading and error states:

AuthBuilder(
  builder: (context, user) => user != null ? MainApp() : LoginScreen(),
  loadingBuilder: (context) => LoadingScreen(),
  errorBuilder: (context, error) => ErrorScreen(error: error),
)

2. Validate Input

Validate email and password formats:

String? validateEmail(String email) {
  if (email.isEmpty) return 'Email is required';
  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(email)) {
    return 'Please enter a valid email';
  }
  return null;
}

String? validatePassword(String password) {
  if (password.isEmpty) return 'Password is required';
  if (password.length < 8) return 'Password must be at least 8 characters';
  return null;
}

3. Secure Token Storage

InstantDB automatically handles secure token storage, but you can access tokens if needed:

final authToken = db.auth.authToken;
if (authToken != null) {
  // Token is available for API calls
}

4. Handle Authentication Errors

Provide clear error messages:

void handleAuthError(InstantException error) {
  String userMessage;
  switch (error.code) {
    case 'invalid_email':
      userMessage = 'Please enter a valid email address';
      break;
    case 'weak_password':
      userMessage = 'Password is too weak. Please use a stronger password';
      break;
    case 'auth_error':
      userMessage = 'Authentication failed. Please try again';
      break;
    default:
      userMessage = 'An unexpected error occurred';
  }
  
  showDialog(
    context: context,
    builder: (context) => AlertDialog(
      title: const Text('Authentication Error'),
      content: Text(userMessage),
      actions: [
        TextButton(
          onPressed: () => Navigator.pop(context),
          child: const Text('OK'),
        ),
      ],
    ),
  );
}

Next Steps

Learn more about authentication features:

On this page