Framework guide
Flutter & Dart
Set up Neuctra Authix in a Flutter app: one client, sessions that survive a restart, an auth gate for protected screens, and paged data in a ListView.
Only publishable keys ship in an app
A binary can be decompiled and --dart-define values are compiled in as plain text, so treat anything you pass that way as public. Use pk_live_…. The SDK throws at construction if you pass a secret key as publishableKey, but only if you do not work around it.
Install
Bashflutter pub add neuctra_authix_dart_package flutter_secure_storage # or add them by hand # neuctra_authix_dart_package: ^2.0.0 # flutter_secure_storage: ^9.0.0Pass your keys at build time
Dart has no runtime
.env, so credentials are compiled in with--dart-define.Bashflutter run \ --dart-define=AUTHIX_APP_ID=app_xxxxxxxxxxxx \ --dart-define=AUTHIX_PUBLISHABLE_KEY=pk_live_xxxxxxxx_xxxxxxxxxxxxxxxx # Or keep them in a file and pass it once flutter run --dart-define-from-file=authix.jsonCreate the client and persist the session
In a browser the user agent stores the session cookie automatically. Dart has no cookie jar, so the SDK ships one — and it lives in memory, which means you decide where it is saved.
Dart1// lib/services/authix.dart 2import 'package:flutter_secure_storage/flutter_secure_storage.dart'; 3import 'package:neuctra_authix_dart_package/neuctra_authix_dart_package.dart'; 4 5/// One client for the whole app. Each client keeps its own cookie jar, so 6/// creating them per screen means signing in on one leaves the rest signed out. 7final authix = NeuctraAuthix( 8 const NeuctraAuthixConfig( 9 appId: String.fromEnvironment('AUTHIX_APP_ID'), 10 publishableKey: String.fromEnvironment('AUTHIX_PUBLISHABLE_KEY'), 11 appName: 'MyApp', 12 ), 13); 14 15const _storage = FlutterSecureStorage(); 16const _sessionKey = 'authix_session'; 17 18/// The cookie jar lives in memory, so it must be written somewhere durable 19/// after every sign-in. 20Future<void> saveSession() async { 21 final cookie = authix.session.sessionCookie; 22 if (cookie != null) { 23 await _storage.write(key: _sessionKey, value: cookie); 24 } 25} 26 27Future<void> restoreSession() async { 28 final saved = await _storage.read(key: _sessionKey); 29 if (saved != null) authix.session.setSessionCookie(saved); 30} 31 32Future<void> forgetSession() => _storage.delete(key: _sessionKey);Why secure storage
That cookie is a bearer token: whoever holds it is that user until it expires.
SharedPreferencesis not an appropriate home for it.Hold one piece of auth state
Dart1// lib/services/auth_state.dart 2import 'package:flutter/foundation.dart'; 3 4/// One piece of state the whole UI follows. Swapping it re-renders the gate. 5final loggedIn = ValueNotifier<bool>(false);Decide the first screen at startup
Dart1// lib/main.dart 2import 'package:flutter/material.dart'; 3 4import 'services/authix.dart'; 5import 'services/auth_state.dart'; 6import 'screens/auth_gate.dart'; 7 8Future<void> main() async { 9 WidgetsFlutterBinding.ensureInitialized(); 10 11 await restoreSession(); // put the saved cookie back 12 13 // Never throws: offline, expired or revoked all resolve to false. 14 final session = await authix.checkUserSession(); 15 loggedIn.value = session.authenticated; 16 17 runApp(const MyApp()); 18} 19 20class MyApp extends StatelessWidget { 21 const MyApp({super.key}); 22 23 24 Widget build(BuildContext context) { 25 return const MaterialApp(home: AuthGate()); 26 } 27}Do not use hasSession to decide this
session.hasSessiononly reports that a cookie exists locally — not that it is still valid. On Flutter Web it is always false, because the browser holds the cookie.checkUserSession()is the only answer to trust.Gate your screens
Dart1// lib/screens/auth_gate.dart 2import 'package:flutter/material.dart'; 3 4import '../services/auth_state.dart'; 5import 'home_screen.dart'; 6import 'login_screen.dart'; 7 8class AuthGate extends StatelessWidget { 9 const AuthGate({super.key}); 10 11 12 Widget build(BuildContext context) { 13 return ValueListenableBuilder<bool>( 14 valueListenable: loggedIn, 15 builder: (context, isLoggedIn, _) => 16 isLoggedIn ? const HomeScreen() : const LoginScreen(), 17 ); 18 } 19}Sign-in now ends with
loggedIn.value = trueand the app moves itself — no manual navigation, and no route left behind that a back gesture can return to.
Signing in
1// lib/screens/login_screen.dart — the submit handler
2Future<void> signIn(String email, String password) async {
3 try {
4 await authix.loginUser(LoginParams(email: email, password: password));
5 await saveSession();
6 loggedIn.value = true; // the gate swaps screens
7 } on AuthenticationError {
8 showError('Wrong email or password');
9 } on NetworkError {
10 showError('Could not reach the server');
11 }
12}Signing up and verifying
Signup issues a session immediately, but the account still needs verifying before it can store anything.
1Future<void> signUp(String name, String email, String password) async {
2 await authix.signupUser(
3 SignupParams(name: name, email: email, password: password),
4 );
5
6 // Signup signs the user in already — the server issues the cookie.
7 await saveSession();
8
9 // But the account is not verified, and an unverified account cannot store
10 // records. Send the code straight away rather than failing on the first write.
11 final session = await authix.checkUserSession();
12 await authix.requestEmailVerificationOTP(
13 userId: session.userId!,
14 email: email,
15 );
16}
17
18Future<void> confirmCode(String email, String otp) async {
19 await authix.verifyEmail(email: email, otp: otp);
20 loggedIn.value = true;
21}Signing out
1Future<void> signOut() async {
2 try {
3 await authix.logoutUser();
4 } catch (_) {
5 // Ignore — the local session is cleared either way, so a logout on a bad
6 // connection cannot leave a usable session on the device.
7 }
8 await forgetSession();
9 loggedIn.value = false;
10}Paged data in a ListView
Reads are bounded — 20 records by default, 100 maximum — so a list fetches the next page as the reader reaches the end.
1// lib/screens/notes_screen.dart
2class NotesScreen extends StatefulWidget {
3 const NotesScreen({super.key, required this.userId});
4 final String userId;
5
6
7 State<NotesScreen> createState() => _NotesScreenState();
8}
9
10class _NotesScreenState extends State<NotesScreen> {
11 final _notes = <DataItem>[];
12 String? _cursor;
13 bool _hasMore = true;
14 bool _loading = false;
15
16 Future<void> _load() async {
17 if (_loading || !_hasMore) return;
18 setState(() => _loading = true);
19
20 final page = await authix.getUserData<DataItem>(
21 GetUserDataParams(
22 userId: widget.userId,
23 category: 'notes',
24 limit: 20,
25 cursor: _cursor,
26 ),
27 itemParser: DataItem.fromJson,
28 );
29
30 setState(() {
31 _notes.addAll(page); // a Page is an Iterable of its records
32 _cursor = page.nextCursor;
33 _hasMore = page.hasMore;
34 _loading = false;
35 });
36 }
37
38
39 void initState() {
40 super.initState();
41 _load();
42 }
43
44
45 Widget build(BuildContext context) {
46 return ListView.builder(
47 itemCount: _notes.length + (_hasMore ? 1 : 0),
48 itemBuilder: (context, index) {
49 if (index == _notes.length) {
50 _load(); // reached the end — fetch the next page
51 return const Center(child: CircularProgressIndicator());
52 }
53
54 final note = _notes[index];
55 return ListTile(
56 title: Text(note['title'] as String? ?? ''),
57 subtitle: Text('v${note.version}'),
58 );
59 },
60 );
61 }
62}For a simple screen, the iterate* methods hand you a Stream and follow the cursor themselves:
1// Or let the SDK follow the cursor and stream records as they arrive.
2StreamBuilder<DataItem>(
3 stream: authix.iterateUserData<DataItem>(
4 GetUserDataParams(userId: userId, category: 'notes'),
5 itemParser: DataItem.fromJson,
6 maxPages: 20,
7 ),
8 builder: (context, snapshot) {
9 // ... accumulate snapshot.data as it arrives
10 },
11);When a session dies mid-use
Tokens expire and password changes revoke every session, so a call can fail while the gate still says “signed in”.
1/// Tokens expire, and changing a password revokes every session for that user.
2/// So a call can fail even when the gate says "signed in". Handle it once.
3Future<T> guarded<T>(Future<T> Function() call) async {
4 try {
5 return await call();
6 } on NoUserSessionError {
7 await forgetSession();
8 loggedIn.value = false; // the gate returns to the login screen
9 rethrow;
10 }
11}
12
13// use it
14final profile = await guarded(
15 () => authix.getUserProfile(userId: currentUserId),
16);Flutter Web
One extra line, because the browser owns cookies there.
1import 'package:http/browser_client.dart';
2
3// On Flutter Web the browser owns the cookie jar and JavaScript cannot read an
4// HttpOnly cookie. Hand the client a BrowserClient and skip persistence.
5final authix = NeuctraAuthix(
6 config,
7 client: BrowserClient()..withCredentials = true,
8);CORS on web
Credentialed cross-origin requests require an explicit allowed origin — a wildcard is rejected by the browser. Add your web origin to the app's allowed origins in the dashboard.
Project structure
Where each piece lands in a typical Flutter project.
lib/
├── main.dart # restore session, then decide the first screen
├── services/
│ ├── authix.dart # one client + secure-storage persistence
│ └── auth_state.dart # ValueNotifier<bool> loggedIn
└── screens/
├── auth_gate.dart # picks Home or Login
├── login_screen.dart
├── signup_screen.dart # signup + OTP verification
├── home_screen.dart
└── notes_screen.dart # paged readsWhat the gate is and is not
The server owns access
The auth gate decides what renders. It is not what keeps one user out of another user's data — a patched binary could show HomeScreen without signing in, and every call it made would still be rejected, because requests carrying a publishable key are pinned to the signed-in session on the server.
Common mistakes
- Creating a client per screen. Each gets its own cookie jar, so sessions stop being shared.
- Forgetting to save the cookie after login. The jar is in memory; the user is signed out on next launch.
- Deciding auth from
hasSession. UsecheckUserSession(). - Skipping email verification. Writes fail with
PermissionDeniedErroruntil the account is verified. - Shipping a secret key. A
--dart-defineis not a secret store.
You now have
- Signup with verification, login and logout.
- A session that survives an app restart.
- Screens gated on real auth state.
- A paged list that will not load a whole dataset into memory.
Related