Flutter rewrite of the Hosanna musician app (previously React + Capacitor). This is now the primary codebase.
- Flutter 3.47.2 (stable) / Dart 3.13.2.
- Application id:
com.embrace.hosanna(AndroidapplicationId/namespace, iOSPRODUCT_BUNDLE_IDENTIFIER). - Android
minSdk 24,targetSdk/compileSdk= Flutter defaults (SDK 36). - iOS deployment target
15.0.
flutter pub get
flutter gen-l10n # regenerate if ARB files change
dart run build_runner build # regenerate Drift code if tables change
flutter runConfiguration is injected at compile time via --dart-define:
| Key | Purpose | Default |
|---|---|---|
HOSANNA_API_URL |
Backend origin (no trailing slash) | https://api.hosanna.live |
HOSANNA_ORIGIN |
Origin header sent on state-changing requests |
http://localhost |
HOSANNA_GOOGLE_SERVER_CLIENT_ID |
Google web/server OAuth client ID used for native sign-in | (unset) |
HOSANNA_GOOGLE_NONCE |
Send a nonce with the Google ID token (see below) | false |
flutter run \
--dart-define=HOSANNA_API_URL=https://your-api.example.com \
--dart-define=HOSANNA_GOOGLE_SERVER_CLIENT_ID=1234.apps.googleusercontent.com \Turnstile is required. The backend enforces Cloudflare Turnstile on
/sign-up/email,/sign-in/email, and/request-password-reset. The app loads the Studio captcha page athttps://studio.hosanna.live/captcha.
Feature-first, layered:
lib/
app/ bootstrap, routing (go_router), theming, DI/providers, settings
core/
config/ compile-time AppConfig
network/ dio client + interceptors (cookies, bearer, captcha, errors)
auth/ session model, secure session store, captcha + social-auth seams
sync/ generic replication engine + per-resource adapters
db/ Drift (SQLite) tables + database
features/ auth, songs, folders, services, metronome, circle_of_fifths, export
shared/ reusable widgets
l10n/ ARB files (pt default, en, es) + generated AppLocalizations
Key libraries: Riverpod (state/DI), dio (HTTP), Drift (SQLite,
reactive watch() streams), go_router (navigation), intl +
flutter_localizations (l10n), flutter_secure_storage (session/bearer),
cloud_firestore (notes pings + live song annotation metadata),
wakelock_plus (keep-awake), webview_flutter (Turnstile).
When Settings → Sync annotations is on, FlueraCanvas annotations for a
service song sync via Firestore (services/{serviceId}/annotations/{songId}),
with the Hosanna API mediating writes (Admin → Firestore; large canvases stay
in Postgres). A local .fcv file remains the offline cache. See the server doc
Hosanna-server/docs/ANNOTATIONS_FIREBASE.md for env vars and rules deploy.
The client is a hand-rolled Better Auth client, so social sign-in is just
another call to /api/auth/sign-in/social:
SocialSignInButton → SocialAuthProvider → native Google (Credential Manager)
→ Google ID token → AuthRepository.signInWithSocial → POST /api/auth/sign-in/social
→ Better Auth verifies the token, finds/creates the user, issues the session
The button on both /sign-in and /sign-up renders the platform's own Google
account chooser (no browser, no WebView). Google is just the first
implementation of SocialAuthProvider (lib/core/auth/social_auth_provider.dart);
Apple/GitHub/Microsoft only need a provider registered in
socialAuthProvidersProvider (lib/app/providers.dart).
Required configuration:
--dart-define=HOSANNA_GOOGLE_SERVER_CLIENT_ID=<web client id>— the web/server OAuth client ID, passed to Google as the ID token audience.- Android OAuth client in Google Cloud Console with package name
com.embrace.hosannaand the SHA-1/SHA-256 of each signing key (debug and release). A missing/incorrect fingerprint surfaces as aGoogleSignInExceptionCode.canceledorclientConfigurationError. - The backend's
socialProviders.google.clientIdmust accept that web client ID (Better Auth supports an array, e.g. web + iOS + Android client IDs). The Google client secret stays on the server; it is never shipped in the app and no server change is needed for the native ID-token flow.
HOSANNA_GOOGLE_NONCE=true additionally mints the ID token with a nonce and
sends it to Better Auth (idToken.nonce), which rejects a token whose nonce
claim does not match. It is off by default because google_sign_in accepts a
nonce only in its one-shot initialize() call, so the value is fixed per app
run and must match whatever Credential Manager has cached. Enable it only once
the server-side flow is confirmed to expect a nonce.
The client never decodes the ID token, never reads profile data from it, and never logs token material — the server is the only component that trusts it.
.github/workflows/ci.yml—flutter analyze+flutter teston push/PR..github/workflows/android-release.yml— builds a signed release APK (split per ABI) and App Bundle, then publishes them to a GitHub Release on tags (v*) or manual dispatch. Requires secretsKEYSTORE_BASE64,KEYSTORE_PASSWORD,KEY_ALIAS,KEY_PASSWORD,API_URL,GOOGLE_SERVER_CLIENT_ID,GITHUB_TOKEN.