Learn how to implement secure email verification using temporary deep links that work seamlessly across web and mobile platforms. This comprehensive guide covers everything from backend setup to Flutter integration.
Email verification with deep linking allows users to verify their email addresses by clicking a link that opens directly in your app, providing a seamless experience across web and mobile platforms. Instead of traditional web-only verification, users get a native app experience with proper navigation and state management.
User signs up with email address in your Flutter app
Backend creates temporary verification link using Redirectly API
Verification link sent to user's email address
User clicks link, app opens, email gets verified
First, create an endpoint that handles user registration and triggers email verification:
// Node.js/Express example const axios = require('axios'); const crypto = require('crypto'); // Initialize Redirectly client const redirectly = axios.create({ baseURL: 'https://redirectly.app/api', headers: { 'Authorization': `Bearer ${process.env.REDIRECTLY_API_KEY}`, 'Content-Type': 'application/json' } }); app.post('/api/register', async (req, res) => { try { const { email, password, username } = req.body; // 1. Create user account (unverified) const user = await createUser({ email, password: await hashPassword(password), username, email_verified: false }); // 2. Generate verification token const verificationToken = crypto.randomBytes(32).toString('hex'); await storeVerificationToken(user.id, verificationToken); // 3. Create temporary verification link const verificationLink = await createVerificationLink(user.id, verificationToken); // 4. Send verification email await sendVerificationEmail(email, verificationLink); res.json({ success: true, message: 'Registration successful. Please check your email to verify your account.', user: { id: user.id, email, username } }); } catch (error) { console.error('Registration error:', error); res.status(500).json({ error: 'Registration failed' }); } });
Create a function that generates temporary verification links using Redirectly:
async function createVerificationLink(userId, token) { try { // Create temporary link that expires in 24 hours const response = await redirectly.post('/v1/temp-links', { target: `https://yourapp.com/verify-email?token=${token}&user=${userId}`, ttlSeconds: 86400 // 24 hours }); return response.data.url; } catch (error) { console.error('Failed to create verification link:', error); throw new Error('Failed to create verification link'); } } async function storeVerificationToken(userId, token) { // Store token in database with expiration await db.query( 'INSERT INTO email_verification_tokens (user_id, token, expires_at) VALUES (?, ?, ?)', [userId, token, new Date(Date.now() + 24 * 60 * 60 * 1000)] ); }
Create an endpoint that handles the verification when the link is clicked:
app.get('/api/verify-email', async (req, res) => { try { const { token, user } = req.query; // 1. Validate token const tokenRecord = await db.query( 'SELECT * FROM email_verification_tokens WHERE token = ? AND user_id = ? AND expires_at > NOW()', [token, user] ); if (!tokenRecord.length) { return res.status(400).json({ error: 'Invalid or expired verification token' }); } // 2. Mark email as verified await db.query( 'UPDATE users SET email_verified = true WHERE id = ?', [user] ); // 3. Delete used token await db.query( 'DELETE FROM email_verification_tokens WHERE token = ?', [token] ); // 4. Return success response res.json({ success: true, message: 'Email verified successfully' }); } catch (error) { console.error('Email verification error:', error); res.status(500).json({ error: 'Verification failed' }); } });
Configure your Flutter app to handle incoming verification links:
import 'package:flutter_redirectly/flutter_redirectly.dart'; class EmailVerificationService { static final FlutterRedirectly _redirectly = FlutterRedirectly(); static Future<void> initialize() async { await _redirectly.initialize(RedirectlyConfig( apiKey: 'YOUR_API_KEY', baseUrl: 'https://redirectly.app', enableDebugLogging: true, )); // Listen for incoming links _redirectly.onLinkClick.listen(_handleIncomingLink); // Check for initial link (app opened from link) final initialLink = await _redirectly.getInitialLink(); if (initialLink != null) { _handleIncomingLink(initialLink); } } static void _handleIncomingLink(LinkClickEvent event) { if (event.error != null) { print('Link error: ${event.error}'); return; } final uri = Uri.parse(event.originalUrl); // Check if this is an email verification link if (uri.path.contains('/verify-email')) { final token = uri.queryParameters['token']; final userId = uri.queryParameters['user']; if (token != null && userId != null) { _processEmailVerification(token, userId); } } } static Future<void> _processEmailVerification(String token, String userId) async { try { // Call your backend to verify the email final response = await http.get( Uri.parse('${ApiConfig.baseUrl}/api/verify-email?token=$token&user=$userId'), headers: {'Content-Type': 'application/json'}, ); if (response.statusCode == 200) { // Show success message _showVerificationSuccess(); // Update app state await AuthService.updateEmailVerificationStatus(true); // Navigate to main app Get.offAllNamed('/home'); } else { _showVerificationError('Verification failed. Please try again.'); } } catch (e) { _showVerificationError('Network error. Please check your connection.'); } } static void _showVerificationSuccess() { Get.snackbar( 'Success', 'Your email has been verified successfully!', backgroundColor: Colors.green, colorText: Colors.white, duration: Duration(seconds: 3), ); } static void _showVerificationError(String message) { Get.snackbar( 'Error', message, backgroundColor: Colors.red, colorText: Colors.white, duration: Duration(seconds: 5), ); } }
Initialize the email verification service when your app starts:
void main() async { WidgetsFlutterBinding.ensureInitialized(); // Initialize email verification service await EmailVerificationService.initialize(); runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( title: 'My App', theme: ThemeData( primarySwatch: Colors.blue, ), home: AuthService.isLoggedIn() ? HomeScreen() : LoginScreen(), routes: { '/home': (context) => HomeScreen(), '/login': (context) => LoginScreen(), '/verify-email': (context) => EmailVerificationScreen(), }, ); } }
// Jest test example describe('Email Verification', () => { test('should create verification link successfully', async () => { const mockUser = { id: '123', email: 'test@example.com' }; const mockToken = 'valid-token-123'; const link = await createVerificationLink(mockUser.id, mockToken); expect(link).toContain('redirectly.app'); expect(link).toContain(mockToken); }); test('should reject expired tokens', async () => { const expiredToken = 'expired-token'; const userId = '123'; const response = await request(app) .get(`/api/verify-email?token=${expiredToken}&user=${userId}`); expect(response.status).toBe(400); expect(response.body.error).toContain('expired'); }); test('should prevent token reuse', async () => { const token = 'used-token'; const userId = '123'; // First verification should succeed const firstResponse = await request(app) .get(`/api/verify-email?token=${token}&user=${userId}`); expect(firstResponse.status).toBe(200); // Second verification should fail const secondResponse = await request(app) .get(`/api/verify-email?token=${token}&user=${userId}`); expect(secondResponse.status).toBe(400); }); });
You now have everything you need to implement secure email verification with deep linking in your Flutter app. This approach provides a seamless user experience while maintaining high security standards.