Skip to content
Back to archipelago.banuacoder.com

Feature SDKs (Free)

auth_sdk Free ​

Tier
Freeversion 3.1.4
Command
archipelago generate auth_sdk
Generates
  • features/auth/auth_api
  • features/auth/auth_impl
Generates first
storage_sdkwhen needed and missing, so there is nothing to install first

Authentication feature SDK with login/register pages, event-driven listener pattern, scoped DI, and per-feature slang translations.

Variables ​

VariableTypeDefaultDescription
appNamestringMyAppThe name of your application
prefixstringAppPrefix for generated classes (e.g. App, My)
uiKitPackageNamestringapp_ui_kitName of the UI kit package (resolved from archipelago.yaml; auto-detected from packages/*_ui_kit when not recorded)
isForMonorepobooleantrueWhether this will be part of a monorepo structure
stateManagementenumblocState management solution: bloc, cubit, riverpod, provider
authBackendenumrest_apiAuth backend provider: supabase, firebase, rest_api
includeAnonymousAuthbooleanfalseGenerate working anonymous (guest) sign-in, plus AuthStatus.anonymous
includePhoneOtpbooleanfalseGenerate working phone-OTP sign-in
isUsingMonitoringbooleanfalseReport auth failures through AppMonitoring

Backend Support ​

The authBackend variable controls which authentication provider the generated datasource and repository use.

  • Supabase — uses supabase_flutter (added to auth_impl's pubspec). Token refresh is handled automatically by the Supabase client. Sign-in via signInWithPassword, registration via signUp.
  • Firebase — uses firebase_auth (added to auth_impl's pubspec). Token refresh is explicit via getIdToken(true). Sign-in via signInWithEmailAndPassword, registration via createUserWithEmailAndPassword.
  • REST API — speaks the vendor-agnostic HttpClient port from network_sdk_api (works with either network_sdk_dio or network_sdk_http, whichever the project's networkLibrary choice registered — since 2.1.1, previously hardcoded to dio and broken under networkLibrary: http) + storage_sdk (StorageProvider, secure/user-scoped namespace backed by flutter_secure_storage with Secure Enclave keys on iOS by default). Calls POST /auth/login, POST /auth/register, and POST /auth/refresh. Access and refresh tokens are stored securely, through a single AuthTokenStore owner (see below), and swept automatically on logout. storage_sdk is 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_impl exports initializeSupabaseAuth(url:, anonKey:). The hook writes apps/<app>/lib/host/initializers/supabase_auth_initializer.dart and adds SupabaseAuthInitializer() to the archipelago:pre_launch_initializers region. It is sequential, so it runs before every parallel pre-launch step and before the sweep. It reads AppEnv.supabaseUrl and AppEnv.supabaseAnonKey, so the hook also adds app_config to the app and declares two env keys in shared/app_config (Env, AppEnv, .env.example and every .env* file present):

    KeyPlaceholder
    SUPABASE_URLhttps://replace-with-your-project.supabase.co
    SUPABASE_ANON_KEYreplace-with-your-supabase-anon-key

    melos run env:init writes the same placeholders. Put your project's URL and anon key (Supabase dashboard, Project Settings > API) in each .env.<flavor>, then run melos run env:switch -- <flavor> and melos run build.

  • Firebase — the hook fills the host's archipelago:firebase:init region in bootstrap.dart with a guarded Firebase.initializeApp() and adds firebase_core to the app, the same way Crashlytics does. It runs before initializeInjector(). This also works in a project generated with isUsingFirebase: false. Run flutterfire configure in 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:

bash
archipelago generate auth_sdk   # answer authBackend: supabase

Mason 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 backendWhat 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.
Supabasesupabase_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.
FirebaseThe 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 in archipelago.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 .env and .env.<flavor> the hook removes a SUPABASE_* 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:

bash
melos bootstrap
melos run build

melos 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 ​

bash
archipelago generate auth_sdk

Non-interactive (CI) ​

bash
archipelago generate auth_sdk --config my_config.json

Generated Structure ​

text
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.yaml

Per-Feature Translations ​

Auth has its own slang configuration with feature-scoped translations:

yaml
# slang.yaml
translate_var: authL10n       # Access via context.authL10n
enum_name: AuthLocale
class_name: AuthTranslations

Translation strings cover login, register, and validation messages in English and Indonesian.

Key Features ​

  • Event-driven auth — AuthSDK emits events (login success, logout) that the app listens to reactively
  • Dual-GetIt DI — Root (AuthSDK, repository, datasource, token store) + authContainer, a FeatureContainer holding the presentation use cases for the shell page's lifetime. auth_imports.dart forwards LoginRepository and AuthSDK; 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; AuthGuard navigates
  • Locale sync — Shell page maps global AppLocale → AuthLocale for 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.initState via a setRepository() 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 ​

dart
// 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:

dart
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:

dart
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 when stateManagement == bloc
  • isCubit — true when stateManagement == cubit
  • usesBloc — true when isBloc || isCubit (both use flutter_bloc)
  • isRiverpod — true when stateManagement == riverpod
  • isProvider — true when stateManagement == 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.

dart
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):

VariableTypeDefaultDescription
includeAnonymousAuthbooleanfalseGenerate anonymous sign-in support with AuthStatus.anonymous
dart
await getIt<AuthSDK>().signInAnonymously();
// AuthSDK.currentStatus == AuthStatus.anonymous

AuthStatus.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):

VariableTypeDefaultDescription
includePhoneOtpbooleanfalseGenerate 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:

RoutePageWhat it does
/auth/phonePhoneLoginPageThe user enters a number in E.164 format (+6281234567890) and taps Send code
/auth/phone/verifyPhoneOtpVerifyPageThe 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-456 fills all six cells. A code that the keyboard pastes is handled the same way.
  • Resend. The resend link appears after the cooldown that requestPhoneOtp returns (resendCooldownSeconds, 60 by default).
  • Errors. Each PhoneOtpFailureReason has its own message, in English and Indonesian (otpErrors in auth_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:

dart
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-exports
  • slang + slang_flutter — Per-feature translations
  • slang_build_runner (dev) — Code generation
  • storage_sdk_api (REST API backend only) — storage_sdk is self-generated automatically if not already installed, so StorageProvider is always available for the REST API datasource's AuthTokenStore

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.