Feature SDKs (Free)
auth_sdk Free
- Tier
- Freeversion 3.1.4
- Command
archipelago generate auth_sdk- Generates
features/auth/auth_apifeatures/auth/auth_impl
- Generates first
- storage_sdkwhen needed and missing, so there is nothing to install first
- Depends on
- shared_identifiers, core_infrastructure_package, flutter_l10ngenerate these first
- Used by
- home_sdk, introduction_sdk, social_auth_sdk
Authentication feature SDK with login/register pages, event-driven listener pattern, scoped DI, and per-feature slang translations.
Variables
| Variable | Type | Default | Description |
|---|---|---|---|
| appName | string | MyApp | The name of your application |
| prefix | string | App | Prefix for generated classes (e.g. App, My) |
| uiKitPackageName | string | app_ui_kit | Name of the UI kit package (resolved from archipelago.yaml; auto-detected from packages/*_ui_kit when not recorded) |
| isForMonorepo | boolean | true | Whether this will be part of a monorepo structure |
| stateManagement | enum | bloc | State management solution: bloc, cubit, riverpod, provider |
| authBackend | enum | rest_api | Auth backend provider: supabase, firebase, rest_api |
| includeAnonymousAuth | boolean | false | Generate working anonymous (guest) sign-in, plus AuthStatus.anonymous |
| includePhoneOtp | boolean | false | Generate working phone-OTP sign-in |
| isUsingMonitoring | boolean | false | Report auth failures through AppMonitoring |
Backend Support
The authBackend variable controls which authentication provider the generated datasource and repository use.
- Supabase — uses
supabase_flutter(added toauth_impl's pubspec). Token refresh is handled automatically by the Supabase client. Sign-in viasignInWithPassword, registration viasignUp. - Firebase — uses
firebase_auth(added toauth_impl's pubspec). Token refresh is explicit viagetIdToken(true). Sign-in viasignInWithEmailAndPassword, registration viacreateUserWithEmailAndPassword. - REST API — speaks the vendor-agnostic
HttpClientport fromnetwork_sdk_api(works with eithernetwork_sdk_dioornetwork_sdk_http, whichever the project'snetworkLibrarychoice registered — since 2.1.1, previously hardcoded todioand broken undernetworkLibrary: http) +storage_sdk(StorageProvider, secure/user-scoped namespace backed byflutter_secure_storagewith Secure Enclave keys on iOS by default). CallsPOST /auth/login,POST /auth/register, andPOST /auth/refresh. Access and refresh tokens are stored securely, through a singleAuthTokenStoreowner (see below), and swept automatically on logout.storage_sdkis self-generated automatically when this backend is chosen — no manual prerequisite step.
Supabase and Firebase persist sessions inside their own SDKs, so no AuthTokenStore is generated for those backends. hasSession() asks the vendor client (currentSession != null / currentUser != null) instead.
Backend setup
The vendor SDK must exist before AuthSDK.initialize() restores the session in the pre-launch Feature SDK sweep. The post_gen hook wires it:
Supabase —
auth_implexportsinitializeSupabaseAuth(url:, anonKey:). The hook writesapps/<app>/lib/host/initializers/supabase_auth_initializer.dartand addsSupabaseAuthInitializer()to thearchipelago:pre_launch_initializersregion. It is sequential, so it runs before every parallel pre-launch step and before the sweep. It readsAppEnv.supabaseUrlandAppEnv.supabaseAnonKey, so the hook also addsapp_configto the app and declares two env keys inshared/app_config(Env,AppEnv,.env.exampleand every.env*file present):Key Placeholder SUPABASE_URLhttps://replace-with-your-project.supabase.coSUPABASE_ANON_KEYreplace-with-your-supabase-anon-keymelos run env:initwrites the same placeholders. Put your project's URL and anon key (Supabase dashboard, Project Settings > API) in each.env.<flavor>, then runmelos run env:switch -- <flavor>andmelos run build.Firebase — the hook fills the host's
archipelago:firebase:initregion inbootstrap.dartwith a guardedFirebase.initializeApp()and addsfirebase_coreto the app, the same way Crashlytics does. It runs beforeinitializeInjector(). This also works in a project generated withisUsingFirebase: false. Runflutterfire configurein the app for the native config; until then the init fails, the app still boots, and sign-in is unavailable.REST API — nothing to set up; the base URL is
API_BASE_URL.
Switch the backend
The backends are variants inside one auth_impl package, not separate packages. Brick 3.0.0 and later switches the backend in one step. Run the brick again and pick the other authBackend:
archipelago generate auth_sdk # answer authBackend: supabaseMason renders auth_impl again for the new backend. It asks before it overwrites a file that you changed. Then the post_gen hook wires the new backend (see Backend setup) and removes what the old backends added:
| Old backend | What the hook removes |
|---|---|
| REST API | _AuthStorageClearInitializer and its entry in post_launch_initializer.dart, and the imports that only it used. auth_impl's AuthTokenStore and SecureAuthTokenStore files and their tests. |
| Supabase | supabase_auth_initializer.dart and its import and entry in pre_launch_initializer.dart. supabaseUrl and supabaseAnonKey in Env and AppEnv. SUPABASE_URL and SUPABASE_ANON_KEY in .env.example. auth_impl's supabase_auth.dart. |
| Firebase | The Firebase.initializeApp() block in the archipelago:firebase:init region of bootstrap.dart, the firebase_core import, and firebase_core in the app pubspec. The hook does this only when nothing else uses Firebase. |
Firebase is kept when the project was created with Firebase (vendors.firebase: true in archipelago.yaml), when another package depends on a firebase_* package (Crashlytics, Firebase Analytics, FCM push, Remote Config), or when an app file other than bootstrap.dart imports one.
The hook never deletes a file that you changed:
- Each run records the backend and a fingerprint of its
auth_impl-only files inarchipelago.yaml(auth.backend,auth.files). A later switch deletes such a file only when its fingerprint still matches. Formatting changes do not count. A project generated before 3.0.0 has no fingerprints, so its first switch keeps these files and lists them. - The Supabase initializer, the REST initializer and the Firebase block are removed only when they still hold the generated code.
- In
.envand.env.<flavor>the hook removes aSUPABASE_*line only while it still holds the placeholder that the hook wrote. It never edits a real value. It lists those keys as no longer used.
Everything that the hook keeps is printed as a list of steps that you do by hand. A run with the backend that is already wired changes nothing. Native Firebase config (google-services.json, GoogleService-Info.plist, firebase_options.dart) is never deleted; the hook reminds you when Firebase is no longer used. Then regenerate the code:
melos bootstrap
melos run buildmelos run build regenerates env.g.dart and the DI config, which still name the old backend's fields and classes until it runs. Code outside auth_impl that uses a vendor SDK directly (for example Supabase.instance) is not part of the switch.
Usage
Interactive
archipelago generate auth_sdkNon-interactive (CI)
archipelago generate auth_sdk --config my_config.jsonGenerated Structure
features/
└── auth/
├── auth_api/
│ ├── lib/
│ │ ├── auth_api.dart
│ │ └── src/
│ │ ├── auth_event.dart
│ │ ├── auth_sdk.dart # Auth FeatureSDK contract
│ │ ├── auth_status.dart
│ │ ├── enums/
│ │ │ └── logout_reason.dart
│ │ └── typedefs.dart
│ └── pubspec.yaml
└── auth_impl/
├── l10n/ # Per-feature translations
│ ├── auth_en.i18n.json
│ └── auth_id.i18n.json
├── lib/
│ ├── auth_impl.dart
│ └── src/
│ ├── auth_sdk_impl.dart
│ ├── data/
│ │ ├── datasources/
│ │ ├── models/
│ │ └── repositories/
│ ├── di/
│ │ ├── auth_global_module.dart
│ │ ├── auth_container.dart # FeatureContainer
│ │ ├── auth_imports.dart # Root types forwarded in
│ │ └── injector.dart
│ ├── domain/usecases/
│ ├── l10n/
│ │ └── auth_translations.g.dart # Generated by slang
│ ├── presentation/
│ │ ├── auth_shell_page.dart # Locale sync + container lease + state holders
│ │ ├── login/login_page.dart # Uses context.authL10n
│ │ └── register/register_page.dart # Uses context.authL10n
│ └── router/
├── slang.yaml # Auth-specific slang config
├── build.yaml # Includes slang_build_runner:legacy
└── pubspec.yamlPer-Feature Translations
Auth has its own slang configuration with feature-scoped translations:
# slang.yaml
translate_var: authL10n # Access via context.authL10n
enum_name: AuthLocale
class_name: AuthTranslationsTranslation strings cover login, register, and validation messages in English and Indonesian.
Key Features
- Event-driven auth —
AuthSDKemits events (login success, logout) that the app listens to reactively - Dual-GetIt DI — Root (AuthSDK, repository, datasource, token store) +
authContainer, aFeatureContainerholding the presentation use cases for the shell page's lifetime.auth_imports.dartforwardsLoginRepositoryandAuthSDK; the shell builds the login and register state holders from its lease, so pages never read GetIt - Single-flight refresh — concurrent 401s join one attempt (see below)
- Guard-driven redirects — the SDK publishes state;
AuthGuardnavigates - Locale sync — Shell page maps global
AppLocale→AuthLocalefor feature-scoped translations - Localized validation — Form validation messages use
context.authL10n.validation.*
Session lifecycle (since 2.0.0)
The repository is global, and arrives as a provider
LoginRepository, its datasource, and (REST backend only) AuthTokenStore are registered in the global GetIt scope by AuthImplGlobalModule. AuthSDKImpl receives the repository as a provider (LoginRepository Function()), resolved on first use.
Both halves matter:
- Global, because token refresh is driven by the network layer, which runs long before and long after any auth screen. Before 2.0.0 the repository came from
AuthShellPage.initStatevia asetRepository()setter that nothing ever called — so refresh could never succeed, in any generated project, and every 401 signed the user out. - A provider, not an instance, because resolving it during global registration closes a DI cycle: repository → HTTP client → auth interceptor →
AuthSDK. Deferring the lookup breaks the cycle without reintroducing a late-binding setter.
The feature's container (authContainer) keeps only the genuinely screen-scoped presentation use cases. AuthSDK stays an eager FeatureSDK: it restores the session before the first screen.
Refresh is single-flight
// Called for you by the network SDK's auth interceptor on a 401.
final retry = await getIt<AuthSDK>().handleAuthenticationFailed(
statusCode: 401,
endpoint: '/me',
);Concurrent callers join one attempt and all observe its real outcome; the consequences (TokenRefreshFailedEvent, sign-out) run inside that attempt and therefore happen exactly once. A burst of parallel 401s is the normal case when an access token lapses, not an exotic race — and against a rotating-refresh backend, one refresh per 401 hands the server a token it has already rotated away, which it reads as token reuse.
A zone-based re-entrancy guard covers the case where the refresh endpoint itself answers 401: without it the interceptor would join the single flight it is already inside and wait for itself forever.
The transport calls it for you (since 2.1.0)
Until 2.1.0 handleAuthenticationFailed had no caller in a generated project: the engine was complete, single-flight and tested, and nothing turned the key. A generated app therefore never refreshed a lapsed access token and never attached a bearer unless the project author wrote the interceptor themselves.
AuthImplGlobalModule now registers AuthSessionTokenProvider as network_sdk_api's AuthTokenProvider, which the HTTP client resolves:
getIt.registerLazySingleton<AuthTokenProvider>(
() => AuthSessionTokenProvider(getIt<AuthTokenStore>(), getIt.get<AuthSDK>),
);Installing auth_sdk >= 2.1.1 alongside network_sdk >= 2.3.1 is all the wiring required. The registration is lazy and takes the SDK as a provider for the same reason the repository is a provider — LoginRepository → HttpClient → AuthTokenProvider → AuthSDK → LoginRepository is a cycle, and deferring both lookups to the first 401 is what cuts it.
Neither brick names the other: the port lives in network_sdk_api, which the transport owns, and the lookup is optional, so a project with a network SDK and no auth feature is unaffected.
Where accessToken comes from follows authBackend: AuthTokenStore for rest_api, Supabase.instance.client.auth.currentSession for supabase, FirebaseAuth.instance.currentUser.getIdToken() for firebase.
Session endpoints are exempt from the refresh path
The REST datasource marks /auth/login, /auth/register, /auth/refresh, /auth/anonymous and the phone-OTP endpoints AuthExemptRequest, so they carry no bearer and their own 401 never triggers a renewal.
This is not belt-and-braces on top of the zone guard — it covers a case the zone guard cannot. A 401 from /auth/login means wrong password; treating it as a lapsed session starts a refresh that finds no refresh token, fails terminally, and signs the user out, so a mistyped password would emit a session-expired event. The zone marker is scoped to the renewal call it wraps and says nothing about sign-in — and it cannot express "do not attach a stale bearer" at all.
DELETE /auth/account is deliberately not exempt: it is an ordinary authenticated request, and a 401 on it should refresh and retry like any other.
One writer of the token keys (REST backend)
SecureAuthTokenStore is the only class that names access_token / refresh_token, and it writes the refresh token first — so a crash mid-rotation leaves a live refresh token with a stale access token (recoverable on the next 401) rather than the reverse (a stranded session). A generated test scans lib/ and fails if anything else starts writing those keys.
logout() discards the credentials, not just the status field.
Guard-driven auth entry
AuthSDK.requireAuthentication() publishes state and takes no BuildContext — it cannot navigate, so it cannot bypass a guard. Attach the AuthGuard shipped in auth_api to the routes you want gated:
final authGuard = AuthGuard(GetIt.I<AuthSDK>());
registry.register([
AutoRoute(page: HomeRoute.page, path: '/home', guards: [authGuard]),
]);This replaces launchAuthFlow(BuildContext), which called context.router.replaceAll(...) from inside feature code and bypassed every guard a project installed.
State Management
The brick generates state management files based on the stateManagement variable passed from the parent flutter_modular_monorepo brick. Supported values: bloc, cubit, riverpod, provider.
Derived boolean variables computed in pre_gen.dart:
isBloc— true whenstateManagement == blocisCubit— true whenstateManagement == cubitusesBloc— true whenisBloc || isCubit(both useflutter_bloc)isRiverpod— true whenstateManagement == riverpodisProvider— true whenstateManagement == provider
Pages use typed design system extensions (context.colors, context.typography, etc.) instead of Theme.of(context).
In every variant the state holder reports a successful login to AuthSDK; the login page only shows a failed login in a SnackBar (BlocListener for bloc and cubit, ref.listen for riverpod, a ChangeNotifier listener for provider). The provider and riverpod variants ship test/src/presentation/login/login_page_test.dart for that path.
AuthShellPage builds the login and register state holders from its container lease and provides them above the nested router: MultiBlocProvider for bloc and cubit, MultiProvider for provider, and a ProviderScope overriding loginNotifierProvider/registerNotifierProvider for riverpod (both throw unless the shell overrides them). They live as long as the shell. test/src/di/auth_di_test.dart checks the container's isolation, its imports and its teardown.
Auth Completeness Add-ons
Opt-in means opt-in
Since 2.0.0, includeAnonymousAuth and includePhoneOtp gate whole capabilities, not just implementations. With them off, signInAnonymously / requestPhoneOtp / verifyPhoneOtp — and AuthStatus.anonymous — are absent from the generated AuthSDK interface entirely, along with their models, use cases, and datasource/repository members. They are never generated as stubs that return a hardcoded failure: an interface member advertises a capability whether or not it works.
This is a breaking change for projects that consumed those members. Regenerate with the variable set to true.
Delete Account
deleteAccount() is a mandatory addition — no brick variable gates it. Apple App Store and Google Play now require apps offering account creation to also offer account deletion.
await getIt<AuthSDK>().deleteAccount();The implementation calls the backend deletion endpoint, then logs out locally (discarding stored credentials) and emits AuthLogoutEvent with LogoutReason.accountDeleted.
Anonymous Sessions
Enable guest sessions with the includeAnonymousAuth variable (default: false):
| Variable | Type | Default | Description |
|---|---|---|---|
| includeAnonymousAuth | boolean | false | Generate anonymous sign-in support with AuthStatus.anonymous |
await getIt<AuthSDK>().signInAnonymously();
// AuthSDK.currentStatus == AuthStatus.anonymousAuthStatus.anonymous is generated only with this variable on, so isAuthenticated is never true for a state the implementation cannot reach.
Phone OTP Flow
Enable phone OTP with the includePhoneOtp variable (default: false):
| Variable | Type | Default | Description |
|---|---|---|---|
| includePhoneOtp | boolean | false | Generate requestPhoneOtp + verifyPhoneOtp and the phone sign-in pages |
With the variable on, the login page has a Sign in with phone number button. It opens two pages inside the auth shell:
| Route | Page | What it does |
|---|---|---|
/auth/phone | PhoneLoginPage | The user enters a number in E.164 format (+6281234567890) and taps Send code |
/auth/phone/verify | PhoneOtpVerifyPage | The user enters the 6-digit code |
The code page uses the UI kit's OTP input, not a plain text field:
- SMS autofill. The input sets the one-time-code autofill hint. iOS offers the code from Messages above the keyboard. Android offers it through its SMS autofill. The brick adds no SMS Retriever package.
- Paste. A long press on the cells pastes the code from the clipboard. Only the digits are kept, so a copied
123-456fills all six cells. A code that the keyboard pastes is handled the same way. - Resend. The resend link appears after the cooldown that
requestPhoneOtpreturns (resendCooldownSeconds, 60 by default). - Errors. Each
PhoneOtpFailureReasonhas its own message, in English and Indonesian (otpErrorsinauth_impl/l10n/*.i18n.json). - Accessibility. Screen readers hear a translated label, the progress ("3 of 6 digits entered") and the error. The resend link is a button.
A full code is verified at once. The Verify button sends it again, for example after a network error. On success verifyPhoneOtp signs the user in, like the email login: the SDK publishes AuthStatus.authenticated and does not navigate.
One state holder, PhoneOtpCubit / PhoneOtpBloc / PhoneOtpNotifier (by stateManagement), backs both pages. AuthShellPage builds it from its container lease. It calls AuthSDK directly, because the SDK results carry the typed failure reason.
With the mock REST datasource, +15555550123 (or any number that starts with +) gets a code, and 123456 is the right code.
To call the SDK from your own UI:
final sdk = getIt<AuthSDK>();
// Step 1: request a code.
final sent = await sdk.requestPhoneOtp('+6281234567890');
if (sent case PhoneOtpRequestSuccess(:final verificationHandle)) {
// Step 2: verify the code the user entered.
final result = await sdk.verifyPhoneOtp(
verificationHandle: verificationHandle,
code: userEnteredCode,
);
if (result case PhoneOtpVerifyFailure(:final reason)) {
// Show the message for `reason`.
}
}The verificationHandle is an opaque token returned by requestPhoneOtp. Its format depends on the authBackend — for Firebase it is the verificationId; for REST API it is an opaque server-issued handle stored via StorageProvider (storage_sdk).
Dependencies
locale_core— Global locale state and slang re-exportsslang+slang_flutter— Per-feature translationsslang_build_runner(dev) — Code generationstorage_sdk_api(REST API backend only) —storage_sdkis self-generated automatically if not already installed, soStorageProvideris always available for the REST API datasource'sAuthTokenStore
Changelog
Run archipelago brick info auth_sdk to see the latest published version. Enterprise customers can read the full CHANGELOG.md in the brick source repository.