v2 client application for Invoice Ninja.
- Windows: apps.microsoft.com/detail/restricted/9NG93WNXN797
- macOS: testflight.apple.com/join/YY1BZ7uR
- Linux (Snap):
sudo snap install invoiceninja --edge - Linux (AppImage): Releases
- iOS: testflight.apple.com/join/YY1BZ7uR
- Android: play.google.com/apps/testing/com.invoiceninja.admin
- Setting up the app
- Application architecture
- Code generation
- Tests
- Screenshots
- Platforms
- Feature parity
- Credits
- Contributions
- License
# 1. Enable the repo's pre-commit hook (formats staged Dart, mirrors CI)
git config core.hooksPath .githooks
# 2. Install dependencies
flutter pub get
# 3. Generate freezed / json_serializable / drift code
dart run build_runner build --delete-conflicting-outputs
# 4. Run (pick a device)
flutter run -d macos # or -d chrome, or an iOS simulatorDon't have an Invoice Ninja backend? Test against the demo server:
- URL:
demo.invoiceninja.com - Email:
demo@invoiceninja.com - Password:
Password0
To avoid retyping credentials, copy dev.json.example → dev.json and run with
--dart-define-from-file=dev.json (debug/profile only; release builds tree-shake it).
macOS entitlements, platform targets, and the web/WASM assets are documented in
docs/setup.md.
Layered MVVM: ChangeNotifier + ListenableBuilder only.
View (StatelessWidget)
└─ ViewModel (ChangeNotifier)
└─ Repository (single source of truth per entity)
├─ Drift database (local state, watched by streams)
├─ Outbox (offline mutation queue)
└─ Service (HTTP client → /api/v1/...)
Drift is the only thing the UI reads from: the network writes into Drift, the UI
watches Drift. Every write goes through the outbox, so the app keeps working offline;
mutations reconcile with the server later via a strictly-ordered, idempotent drain
loop. Every list is scoped by company_id (multi-company), and on native platforms
Drift sits on SQLCipher (encrypted at rest).
lib/
├── main.dart boot sequence (DI, restore, runApp)
├── app/ DI bag, router, theme, design tokens, env
├── data/
│ ├── models/ freezed domain models + API DTOs
│ ├── services/ HTTP clients (one per entity + shared ApiClient)
│ ├── repositories/ single source of truth per entity
│ └── db/ Drift database, DAOs, tables
├── domain/ entity types, registry, sync dispatchers
├── ui/
│ ├── core/ generic list/detail/edit scaffolds + shared widgets
│ └── features/ feature folders (auth, clients, products, …)
├── l10n/ localization delegate + supported locales
└── utils/ Formatter, parsing helpers, etc.
Every entity follows the same shape under data/ and ui/features/. When in doubt,
mirror Client.
ARCHITECTURE.md, start here: the five layers, the login and create-product code flows, and cross-cutting concerns.CLAUDE.md: strict rules and invariants.FEATURES.md: live parity tracker (React vs Flutter v1 vs this rebuild).BACKEND.md: upstream API gaps this client depends on.docs/, deep dives: adding an entity, settings screens, the offline write pipeline, probing the demo API, integration tests, diagnostics, and setup.
Regenerate freezed models, JSON serializers, and Drift code after editing any annotated source:
dart run build_runner build --delete-conflicting-outputsflutter analyze # static analysis
flutter test # unit + widget + repository-contract suite
flutter build web --wasm # the authoritative web compile gateIntegration tests (integration_test/) boot the real app and take over the
foreground. Let CI run them; don't run them during a focused session. The
on-request procedure is in docs/integration-tests.md.
The images at the top are generated, not hand-captured, produced by
integration_test/screenshots_test.dart, which boots the real app (web) against the
demo server and captures each marketing screen. This is a local, on-demand task
(needs chromedriver):
bash tools/capture_screenshots.sh # writes PNGs into samples/screenshots/https://github.com/invoiceninja/invoiceninja/tree/v5-develop#credits
We gladly accept contributions! To get involved, join our Slack group or Discord server.
See LICENSE.txt.