Skip to content

User Management

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

Email and Password

Traditional email/password authentication:

class EmailPasswordAuth extends StatefulWidget {
@override
State<EmailPasswordAuth> createState() => _EmailPasswordAuthState();
}
class _EmailPasswordAuthState extends State<EmailPasswordAuth> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLoading = false;
String? _errorMessage;
Future<void> _signUp() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final db = InstantProvider.of(context);
final user = await db.auth.signUp(
email: _emailController.text.trim(),
password: _passwordController.text,
metadata: {
'name': 'New User',
'createdAt': DateTime.now().millisecondsSinceEpoch,
},
);
print('User created: ${user.email}');
// Navigation handled by AuthBuilder
} on InstantException catch (e) {
setState(() {
_errorMessage = e.message;
});
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _signIn() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final db = InstantProvider.of(context);
final user = await db.auth.signIn(
email: _emailController.text.trim(),
password: _passwordController.text,
);
print('User signed in: ${user.email}');
} 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',
keyboardType: TextInputType.emailAddress,
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: 'Password',
),
obscureText: true,
),
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),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: _isLoading ? null : _signUp,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Sign Up'),
),
),
const SizedBox(width: 16),
Expanded(
child: OutlinedButton(
onPressed: _isLoading ? null : _signIn,
child: const Text('Sign In'),
),
),
],
),
],
);
}
}

Passwordless authentication via email links:

class MagicLinkAuth extends StatefulWidget {
@override
State<MagicLinkAuth> createState() => _MagicLinkAuthState();
}
class _MagicLinkAuthState extends State<MagicLinkAuth> {
final _emailController = TextEditingController();
bool _isLoading = false;
bool _linkSent = false;
String? _errorMessage;
Future<void> _sendMagicLink() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final db = InstantProvider.of(context);
await db.auth.sendMagicLink(_emailController.text.trim());
setState(() {
_linkSent = true;
});
} on InstantException catch (e) {
setState(() {
_errorMessage = e.message;
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_linkSent) {
return Column(
children: [
const Icon(Icons.mail_outline, size: 64, color: Colors.green),
const SizedBox(height: 16),
const Text(
'Magic link sent!',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Check your email at ${_emailController.text} and click the link to sign in.',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.grey),
),
const SizedBox(height: 24),
OutlinedButton(
onPressed: () {
setState(() {
_linkSent = false;
});
},
child: const Text('Send Another Link'),
),
],
);
}
return Column(
children: [
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email Address',
keyboardType: TextInputType.emailAddress,
),
),
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 : _sendMagicLink,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Send Magic Link'),
),
),
],
);
}
}

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'),
),
],
],
);
}
}

User Profile Management

Update User Metadata

class UserProfile extends StatefulWidget {
final AuthUser user;
const UserProfile({super.key, required this.user});
@override
State<UserProfile> createState() => _UserProfileState();
}
class _UserProfileState extends State<UserProfile> {
final _nameController = TextEditingController();
bool _isLoading = false;
@override
void initState() {
super.initState();
_nameController.text = widget.user.metadata?['name'] ?? '';
}
Future<void> _updateProfile() async {
setState(() {
_isLoading = true;
});
try {
final db = InstantProvider.of(context);
await db.auth.updateUser({
'name': _nameController.text.trim(),
'updatedAt': DateTime.now().millisecondsSinceEpoch,
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Profile updated successfully')),
);
} on InstantException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${e.message}')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
CircleAvatar(
radius: 50,
child: Text(
widget.user.email.substring(0, 1).toUpperCase(),
style: const TextStyle(fontSize: 32),
),
),
const SizedBox(height: 24),
Text(
widget.user.email,
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 24),
TextField(
controller: _nameController,
decoration: const InputDecoration(
labelText: 'Display Name',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isLoading ? null : _updateProfile,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Update Profile'),
),
),
],
);
}
}

Password Reset

class PasswordReset extends StatefulWidget {
@override
State<PasswordReset> createState() => _PasswordResetState();
}
class _PasswordResetState extends State<PasswordReset> {
final _emailController = TextEditingController();
bool _isLoading = false;
bool _emailSent = false;
String? _errorMessage;
Future<void> _resetPassword() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final db = InstantProvider.of(context);
await db.auth.resetPassword(_emailController.text.trim());
setState(() {
_emailSent = true;
});
} on InstantException catch (e) {
setState(() {
_errorMessage = e.message;
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_emailSent) {
return Column(
children: [
const Icon(Icons.mark_email_read, size: 64, color: Colors.green),
const SizedBox(height: 16),
const Text(
'Password reset email sent!',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text(
'Check your email at ${_emailController.text} for reset instructions.',
textAlign: TextAlign.center,
),
],
);
}
return Column(
children: [
const Text(
'Forgot your password?',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
const Text(
'Enter your email address and we\'ll send you a link to reset your password.',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextField(
controller: _emailController,
decoration: const InputDecoration(
labelText: 'Email Address',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.emailAddress,
),
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 : _resetPassword,
child: _isLoading
? const CircularProgressIndicator()
: const Text('Send Reset 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: