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),
),
),
);
}
}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:
- Session Management - Managing user sessions and tokens
- Permissions - Role-based access control
- User Presence - Adding users to collaborative features
- Advanced Auth - Offline authentication handling