A React Native / Expo Android app that demonstrates offline-first note syncing with Firebase Realtime Database. The app keeps notes available while offline, then syncs changes when connectivity returns.
- Separate email/password login and registration flows.
- Notes CRUD: create, edit, delete, and browse notes.
- Offline-aware sync through Firebase Realtime Database.
- Network status tracking with
@react-native-community/netinfo. - Session/theme persistence with Zustand + AsyncStorage across app restarts.
- Per-user data isolation: notes live under
/notes/{uid}and are enforced by Realtime Database security rules (database.rules.json), not client filtering. - Native Firebase configuration for Android through
google-services.json. - Release-safe logging with secret redaction in development.
- Registration password policy enforced (min 8 chars with letter, number, and special character).
The app follows a one-directional layered flow:
DataSource -> Repository -> UseCase -> Store (Zustand) -> UI
- DataSource – the only place Firebase is touched (
data/datasources/). - Repository – a thin boundary that wraps a datasource (
data/repositories/). - UseCase – one domain action each (
domain/usecases/). - Composition root –
features/<feature>/container.jswires datasource → repository → use case once, so stores depend only on ready-to-call use cases and never on how they are assembled. - Store – Zustand state, split by concern (see below). Stores orchestrate use cases and hold no view logic.
- UI – screens/components read from stores and call store actions only.
| Store | Owns | Persisted |
|---|---|---|
shared/store/useUiStore |
theme, global isLoading, connectivity, snackbar feedback |
theme only |
features/auth/store/useAuthStore |
isLogin, usrMail, usrId, login/register/logout/session |
session only |
features/notes/store/useNotesStore |
current note list + note CRUD actions | not persisted (source of truth is Firebase) |
Native confirmation dialogs live in shared/feedback/confirmDialog.js so state
modules stay free of UI concerns.
- Expo / React Native (Android)
- React Navigation (native stack)
- Firebase Realtime Database via the Firebase JS SDK (
firebasepackage) - Zustand (+
persistmiddleware over AsyncStorage) @react-native-community/netinfo- React Native Paper
- Jest (
jest-expo) + React Test Renderer - Runtime code is
.jsannotated with JSDoc;tsconfig.jsonis included so editors surface those types. There is no build-time TS compilation step.
src/
App.js # bootstrap: starts network listener + session sync
features/
auth/
container.js # composition root for auth
data/{datasources,repositories}/
domain/usecases/
screens/ # Login, Register
store/useAuthStore.js
notes/
container.js # composition root for notes
components/ # NoteCard
data/{datasources,repositories}/
domain/usecases/
screens/ # Home, Note
store/useNotesStore.js
settings/
screens/ # Settings
navigation/ # MainNavigation (guest vs. app stacks)
shared/
components/ # Loader, PasswordInput
feedback/ # feedbackAdapter, GlobalSnackbar, confirmDialog
firebase/ # firebaseClient
store/useUiStore.js
utils/ # logger (redacts secrets, silent in release)
Firebase is initialized through the JavaScript SDK in src/shared/firebase/firebaseClient.js.
- app.json points Android to google-services.json for the native build.
firebaseClient.jsalso reads google-services.json at runtime to build theinitializeApp(...)config (apiKey,appId,projectId,databaseURL, …), so there is a single Firebase source of truth for the Android target.
google-services.json is committed on purpose. The values it contains
(apiKey, appId, projectId, databaseURL) are public client identifiers,
not secrets — they ship inside every distributed APK and cannot be hidden from a
determined user. Firebase is designed around this; access control is enforced
server-side, not by keeping the config private.
What actually protects data:
- Firebase Authentication – every request is made as a signed-in user; there is no anonymous access.
- Realtime Database security rules – database.rules.json. Notes are stored
under an owner-scoped path
/notes/{uid}/{noteId}, and the rules grant read and write on/notes/{uid}only whenauth.uid === {uid}. A user cannot read, query, or write another user's notes even with a hand-crafted request; the client-side code never has to be trusted for isolation. Field-level.validaterules also constrain note shape and size.
Deploy the rules with the Firebase CLI (config in firebase.json):
firebase deploy --only databaseOther practices in the codebase:
console.*is disabled entirely in release builds (src/App.js).- src/shared/utils/logger.js redacts
password/token/apiKey/ etc. (including nested and array values) before anything reaches the console in development. - Registration enforces a password policy (min 8 chars, letter + number + special).
- Real secrets (e.g.
EXPO_TOKENfor CI) live in GitHub Actions secrets, never in the repo.
This repo targets Android. iOS (GoogleService-Info.plist) and web (env-based
Firebase config) are not set up.
npm install- Create a Firebase project and enable Realtime Database.
- Download the Android
google-services.jsonand place it at the project root (kept wired throughandroid.googleServicesFilein app.json). - Enable Email/Password sign-in under Authentication.
- Deploy the security rules:
firebase deploy --only database.
npx expo start -cData/auth calls use the Firebase JS SDK, so Android flows run in Expo Go.
EAS build profiles live in eas.json (preview builds an APK, production a release build):
eas build --profile preview --platform androidnpm test # watch mode
npm run test:ci # single run + coverage (fails under the configured threshold)test:ci enforces a global coverage floor (see jest.coverageThreshold in
package.json); framework glue (navigation, the Firebase client,
composition roots) is excluded from the measurement.
| Area | Suites |
|---|---|
| Datasource ↔ Firebase wiring | authRemoteDataSource, notesRemoteDataSource |
| Repository + use-case delegation | authRepository, notesRepository, authUseCases, notesUseCases |
| Auth store | useAuthStore – login/register/logout, weak-password rejection, session sync, cross-store note clearing |
| Notes store | useNotesStore – toNotesList snapshot→array transform, subscription lifecycle, blank-title guard, write-failure feedback |
| Offline / connectivity | useUiStore – startNetworkListener online/offline transitions and the initial NetInfo.fetch() result |
| Screens | Login, Register (validation + submit), Settings (logout), NoteCard (confirm→delete with owner id) |
| Shared | logger secret redaction, feedbackAdapter pub/sub, confirmDialog, Loader, PasswordInput |
APK release automation is configured with the workflow at .github/workflows/build-apk.yml.
- Installs dependencies
- Runs the test suite with coverage (
npm run test:ci) - Builds Android APK with EAS
- Downloads the APK artifact
- Publishes the APK to GitHub Releases
- Push to
main(creates a release with auto tag formatv0.0.0-main-<run_number>) - Tag push matching
v*(for example:v1.0.1) - Manual run from Actions (
workflow_dispatch) with selectable EAS profile
Add this repository secret before running the workflow:
EXPO_TOKEN: Expo access token used by EAS CLI in CI
git tag v1.0.1
git push origin v1.0.1After the workflow finishes, the APK is attached to the matching GitHub Release tag.
- Login / registration screen
- Notes list screen
- Note editor screen
- Settings screen
- The app entry point is src/App.js, which starts the connectivity listener and rehydrates the Firebase session on mount.
- See the Security model section above for how secrets and data isolation are handled.
