Full-Stack Mobile Development (Flutter + Serverpod) #3 - Serverpod Authentication
How to integrate Serverpod Authentication, giving users the ability to sign up, sign in, and reset their password with a validation code.

Hello, I'm Samuel, also known as Tech With Sam.
I am passionate about learning and teaching programming, particularly Flutter and Dart at the moment. Please support me by subscribing to my newsletter. Thanks!
Subscribe for weekly tutorials and tips, or DM me to bring your app idea to life.
Questions? Join me on Discord: https://discord.gg/8X7dPYujqm For Business: techwithsam10@gmail.com
Hey, Flutter devs! It’s TechWithSam again. Today, I will walk you through how to integrate Serverpod Authentication, giving users the ability to sign up, sign in, and reset their password with a validation code.
By episode’s end, you’ll have JWT-powered sessions flowing, with Flutter’s SessionManager handling the heavy lifting — no manual token juggling. We’re keeping it MVP-simple: focus on email authentication for secure trade tasks, and scopes for future admin features (e.g., user vs. trader roles).
Server-Side Setup: Wire Auth into Your Backend
Serverpod’s serverpod_auth module auto-handles hashing, validation, and JWTs — perfect for fintech security without custom crypto nightmares.
1. Add the Auth Module
In fintech_todo_server/pubspec.yaml:
dependencies:
serverpod_auth_server: ^2.9.1 # Matches your Serverpod version
Run:
dart pub get
2. Hook the Authentication Handler
Update fintech_todo_server/bin/server.dart to route auth calls:
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_auth_server/serverpod_auth_server.dart' as auth;
void run(List<String> args) async {
var pod = Serverpod(
args,
Protocol(), // Your protocol
Endpoints(), // Your endpoints
authenticationHandler: auth.authenticationHandler, // <-- Key line: Routes /auth/*
);
await pod.start();
}
3. (Optional) Nickname the Module
For cleaner imports, add to fintech_todo_server/config/generator.yaml:
modules:
serverpod_auth:
nickname: auth
4. Generate Code & Init DB Tables
Regen stubs:
cd fintech_todo_server
serverpod generate
Create/apply migration for auth tables (users, keys, etc.):
serverpod create-migration
docker compose up -d # If not running
dart run bin/main.dart --role maintenance --apply-migrations
5. Configure Auth Settings
In server.dart, set policies (fintech-tight: Long passwords, quick resets):
import 'package:serverpod_auth_server/module.dart' as auth;
void run(List<String> args) async {
// ... pod setup ...
auth.AuthConfig.set(
auth.AuthConfig(
minPasswordLength: 12, // Beefy for trades
emailSignInFailureResetTime: Duration(minutes: 5), // Lockout reset
maxAllowedEmailSignInAttempts: 5, // Anti-brute-force
passwordResetExpirationTime: Duration(hours: 24),
// Hooks: Optional for email sending (use Firebase/SES later)
sendValidationEmail: (email, code) => print('Send to $email: $code'), // Placeholder
sendPasswordResetEmail: (email, resetCode) => print('Reset for $email: $resetCode'),
// Fintech: Disable name edits for compliance
userCanEditFullName: false,
),
);
await pod.start();
}
Config — Why for Fintech MVP? — Default Fallback
minPasswordLength — Blocks weak creds — 8
maxAllowedEmailSignInAttempts— Stops spam logins — 5
sendValidationEmail— Custom emailer (e.g., via API in prod) — None (console log)
Best practice: Never commit secrets — use env vars for prod OAuth.
Client-Side Setup: Flutter Auth Flow
Now, wire Flutter’s SessionManager for our workflow: Check sign-in → Show Login or Home.
1. Add Client Dependencies
In fintech_todo_flutter/pubspec.yaml:
dependencies:
flutter:
sdk: flutter
serverpod_flutter: ^2.5.0
serverpod_auth_client: ^2.5.0
serverpod_auth_shared_flutter: ^2.5.0
fintech_todo_client: # Your generated client
path: ../fintech_todo_client
flutter_secure_storage: ^9.0.0 # For JWT storage
Run:
flutter pub get
2. Init Client & SessionManager
In lib/main.dart (MVP: Localhost for dev; swap for device IP):
import 'package:flutter/material.dart';
import 'package:serverpod_flutter/serverpod_flutter.dart';
import 'package:serverpod_auth_client/serverpod_auth_client.dart';
import 'package:fintech_todo_client/fintech_todo_client.dart' as clientLib;
late SessionManager sessionManager;
late clientLib.Client spClient;
void main() async {
WidgetsFlutterBinding.ensureInitialized();
const ipAddress = 'localhost'; // '10.0.2.2' for Android emulator
spClient = clientLib.Client(
'http://$ipAddress:8080/',
authenticationKeyManager: FlutterAuthenticationKeyManager(),
)..connectivityMonitor = FlutterConnectivityMonitor();
sessionManager = SessionManager(caller: spClient.modules.auth);
await sessionManager.initialize();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Fintech Todo',
home: const AuthWrapper(), // Routes based on sign-in
debugShowCheckedModeBanner: false,
);
}
}
3. Build Auth Screens & Workflow
Core: AuthWrapper checks the sessionManager.isSignedIn → Login or Home.
Login/Register Screen (lib/screens/login_screen.dart):
import 'package:flutter/material.dart';
import 'package:serverpod_auth_client/serverpod_auth_client.dart';
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isLogin = true; // Toggle for register
Future<void> _authAction() async {
try {
if (_isLogin) {
await sessionManager.signIn(email: _emailController.text, password: _passwordController.text);
} else {
// Register: Validate first, then create
final validation = await sessionManager.validateCredentials(
email: _emailController.text,
password: _passwordController.text,
);
if (validation.isValid) {
await sessionManager.createUser(
email: _emailController.text,
password: _passwordController.text,
userName: _emailController.text.split('@')[0], // Simple username
);
await sessionManager.signIn(email: _emailController.text, password: _passwordController.text);
} else {
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Invalid email/password')));
}
}
if (mounted) Navigator.pushReplacementNamed(context, '/home');
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Auth failed: $e')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(_isLogin ? 'Login' : 'Register')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(controller: _emailController, decoration: const InputDecoration(labelText: 'Email')),
TextField(controller: _passwordController, obscureText: true, decoration: const InputDecoration(labelText: 'Password')),
ElevatedButton(onPressed: _authAction, child: Text(_isLogin ? 'Login' : 'Register')),
TextButton(onPressed: () => setState(() => _isLogin = !_isLogin), child: Text(_isLogin ? 'Need an account?' : 'Have one?')),
// Optional Google: await sessionManager.signInWithOAuth(Provider.google);
],
),
),
);
}
}
Home Screen with Auth Guard (lib/screens/home_screen.dart):
import 'package:flutter/material.dart';
class AuthWrapper extends StatefulWidget {
const AuthWrapper({super.key});
@override
State<AuthWrapper> createState() => _AuthWrapperState();
}
class _AuthWrapperState extends State<AuthWrapper> {
@override
void initState() {
super.initState();
sessionManager.addListener(_onAuthChange); // Listen for sign-in/out
}
void _onAuthChange() => setState(() {}); // Rebuild on auth events
@override
Widget build(BuildContext context) {
return sessionManager.isSignedIn
? HomeScreen(user: sessionManager.signedInUser!) // Secure dashboard
: const LoginScreen();
}
}
class HomeScreen extends StatelessWidget {
final UserInfo user;
const HomeScreen({super.key, required this.user});
Future<void> _logout() async {
await sessionManager.signOutDevice(); // Or signOutAllDevices() for global
// Nav back to login auto via listener
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Welcome, ${user.userName}!'), actions: [IconButton(onPressed: _logout, icon: const Icon(Icons.logout))]),
body: const Center(child: Text('Tasks list here—secure & ready!')), // Empty state for Part 4
floatingActionButton: FloatingActionButton(
onPressed: () {}, // Tease CRUD create
child: const Icon(Icons.add),
),
);
}
}
Wrapping Up: Auth Locked, CRUD Next
Boom — your fintech todo’s guarded: Email logins flow to a secure Home, scopes ready for roles, all in pure Dart (no Firebase silos). This nails 80% of auth pains, letting us prototype trade tasks without leaks. Fork the repo here and test a login — share wins in comments!
I hope you are ready to learn something incredible. Press that follow button if you’re not following me yet. Also, make sure to subscribe to the newsletter so you’re notified when I publish a new article. Kindly press the clap button as many times as you want if you enjoy it, and feel free to ask a question.
Catch the Ep.3 on YouTube. Next: Part 4, Task CRUD Operations — building those secure endpoints. What’s your auth horror story? Hit Discord: https://discord.gg/NytgTkyw3R.
Keep stacking secure in 2025 — see you on the dashboard! 🚀
Samuel Adekunle, Tech With Sam YouTube | Part 4 Teaser
Happy Building! 🥰👨💻





