Repository navigation
Implementation
How the DesKilo repository is organized, built, tested, and shipped. The methodology is inherited 1:1 from the tankstellen/Sparkilo project.
android/ # Android runner (Play + Android builds)
ios/ # iOS runner
macos/ # macOS desktop runner
windows/ # Windows runner + WiX MSI authoring (installer/deskilo.wxs)
assets/fonts/ # Roboto, embedded into PDF bills (base-14 fonts can't encode €)
docs/
SPECIFICATION.md # the product spec (source of truth for behavior)
PROJECT_OVERVIEW.md # consolidated reference across all of the below
wiki/ # source of the GitHub wiki pages (this site)
decisions/ # ADRs 0001..0018
design/ # design system, payments integration
guides/ # e.g. RELEASING.md
security/ # SUPABASE_RLS_MATRIX.md
fastlane/ metadata/ # store metadata per locale
lib/ # the app (see Architecture page)
web/ # browser target (GitHub Pages deploy, opt-in)
supabase/
migrations/ # 0001..0190 — schema, RLS, RPCs (numbered, immutable)
functions/ # deployed Edge Functions: create-payment-order,
# paypal-webhook, stripe-webhook, mollie-webhook,
# send-e-invoice
test/ # unit + widget tests, fakes in test/helpers/
integration_test/ # end-to-end flows
tool/ tools/ scripts/ # repo tooling
- Issue-first: no code without a GitHub issue; work larger than one PR becomes an Epic with a validated breakdown.
-
GitHub Flow: branch off
master, conventional commits (feat(scope): …), one concern per branch, every change is a PR < 400 lines (excluding generated), squash-merge,Closes #NN, no direct commits to master. - No hard-coded user-facing strings — ARB only, lint-ratcheted; the key-parity CI gate fails if any locale misses a key.
-
Clean codegen before push — generated files (
*.g.dart,*.freezed.dart,app_localizations*.dart) are committed and must be regenerated, never hand-edited. -
TDD pyramid 70/20/10 (unit/widget/integration) as the intended shape —
dart run tool/test_inventory.dartprints the observed one (#1334) —, coverage gate by layer, fakes over mocks, failing test before any bug fix, twin-bug audit (grep for the same pattern elsewhere), producer + consumer ship together. -
SPDX header (
// SPDX-License-Identifier: AGPL-3.0-or-later) in every file.
Prerequisites: Flutter (pinned stable), Dart ≥ 3.11.
flutter pub get
flutter gen-l10n # localizations
dart run build_runner build --delete-conflicting-outputs # riverpod/freezed/json codegen| Target | Command |
|---|---|
| Android (debug) | flutter run -d <device> |
| Android APK |
flutter build apk (per-ABI splits in release CI) |
| iOS |
flutter run -d <iphone> / TestFlight via fastlane |
| macOS |
flutter run -d macos · release: flutter build macos, app at build/macos/Build/Products/Release/DesKilo.app
|
| Windows |
flutter build windows · MSI: gh workflow run windows-msi.yml -f ref=master (WiX v5 on CI; artifact DesKilo-<version>-windows-msi, attached to releases on v* tags) |
The macOS runner is sandboxed; entitlements already include the network client (Supabase), camera (QR scan), and user-selected file access (XML import/export, PDF save).
- Migrations are numbered SQL files in
supabase/migrations/, applied in order to the hosted reference project (and by self-hosters). They are immutable once applied — fixes are new migrations. -
The file set is the source of truth for a rebuild; the hosted project's migration rows are not. The two can legitimately differ, and both cases in this repo are documented in the file that absorbed them: a single file may have been applied as several rows (
0070_payment_date_and_period.sql= rowspayment_date_and_period+invoice_annex_occurred_on), and a superseded interim draft can leave a row behind with no file at all (0051_single_use_invites_member_validation, replaced by0051_personal_invitations+0052_member_join_validation, whose section 0 drops the draft's leftovers). When a draft is superseded this way, the replacing file must be written to converge from either state —drop policy if existsbeforecreate policy,drop … if existsfor the abandoned objects — so a fresh deploy and the hosted project end up identical. Say so in the file header; a name mismatch that is not explained reads as drift. - Local development is possible with the Supabase CLI (
supabase start), but the reference deployment is hosted Supabase (decided 2026-07-07). - Pattern to follow when adding tables: enable RLS immediately, add select policies with the role-helper functions, keep writes behind
SECURITY DEFINERRPCs, andrevoke execute … from public, anonon every new function (see migration 0004). -
Secrets tables get zero policies.
payment_credentials(0047) enables RLS and adds no policies at all — only service-role Edge Functions and owner RPCs reach it, and the status RPC returns key names, never values. Follow this shape for anything secret. - Edge Functions are deployed to the hosted project (webhooks run with
verify_jwtoff and authenticate via the payment provider's signature instead). Provider credentials resolve table-first, env-fallback, so a single-workspace self-host can configure everything with function env vars alone.
When a screen or surface belongs to a WorkspaceFeature, gate it at both layers (the enable a feature → get all of it; disable → get none rule):
- Hide the entry point:
if (… && features.contains(WorkspaceFeature.x))in the settings list / tab bar. - Guard the route:
featureEnabled(WorkspaceFeature.x)in itsredirectinapp/router.dart.
/services, /payment-config, /nfc-config, and /accessories are the reference implementations; the Features screen itself must stay ungated. Add a widget test for both the hidden entry and the bounced deep link (see test/features/profile/settings_sections_test.dart).
flutter analyze --fatal-infos lib test tool # infos are fatal in CI too
flutter test # full suite (2600+ tests)
flutter test test/features/<feature>/ # one featureRun flutter test bare. Piping it (| tail, | grep) hands you the exit code of the pipe, not of the suite, so a red run reads as green — this has shipped a broken commit more than once.
- Widget tests pump the whole
DeskiloAppwithProviderScope(overrides: standardTestOverrides(...))— fakes for auth, workspace, reservations, money, notifications live intest/helpers/mock_providers.dart. Fakes mirror server behavior including RLS visibility (e.g.FakeWorkspaceRepository.adminInviteCodeonly answers for owners). - Accessibility is tested (
meetsGuideline(androidTapTargetGuideline)), so every new tappable affordance needs a big-enough target and its own test that taps it. - Server SQL semantics are documented in the migration files; client tests assert against the fake seam.
Every push/PR runs CI · Analyze, test & coverage: l10n key-parity
gate · analyze · full test suite · coverage gate (≥ 45 %). Its job is
named analyze · l10n gate · test · coverage, and that job name is the
required status check on master.
Fourteen workflows in all, named <Group> · <what it does> so the
Actions sidebar sorts into gates, releases, publishes, status queries
and tools — the convention, the table of what exists and the rules are
in .github/workflows/README.md, enforced by
test/lint/workflow_naming_test.dart.
The one thing to know before renaming anything there: a workflow name is free to change, a job name is not. A job name is a required status-check context, so renaming one can block auto-merge until branch protection is updated in lockstep.
Release · macOS DMG, Release · Windows MSI, Publish · Web app and
CI · F-Droid no-GMS audit also build on pull requests that touch their
target, so a change that breaks one platform fails there rather than in
front of a user.
Git discipline — branch off master, PRs only, green CI before merge, squash-merge, delete the head branch — is server-enforced since 2026-08-01, and the configuration lives as data in scripts/branch_protection.sh (verify / apply / show) rather than in a console nobody can diff. master requires the analyze · l10n gate · test · coverage check, blocks force-pushes and deletions, and demands linear history. Strict mode is deliberately off (no merge queue → strict makes every merge stale every other open PR, quadratic CI for file-disjoint changes); the corollary is a merge discipline: serialise merges rather than arming several at once. enforce_admins is off, so the maintainer keeps an admin escape hatch — the rules in AGENT_RULES.md still apply to its use. An advisory CI step runs verify so drift between the committed target and the live configuration is reported, not discovered.
- Semver + annotated tag after the release PR merges; release notes generated from PR titles.
- Android: Play (internal → closed → open → production). F-Droid is
supported through the libre flavour (ADR 0012): the app depends on
deskilo_push, and the F-Droid build swaps indeskilo_push_foss, which has no Google dependency. CI · F-Droid no-GMS audit proves it on every pull request; Publish · F-Droid release APKs ships the binaries F-Droid reproduces against (publication: the F-Droid status). FCM is the push transport for store builds only. - iOS: TestFlight via fastlane (owner-held App Store Connect secrets), internal groups plus an external group with a public link.
- Desktop: Windows ships as an MSI from the
windows-msiworkflow, macOS as a Developer-ID-signed, Apple-notarised, stapled DMG frommacos-app— both attached to the release on av*tag. (Spec §12 left the macOS channel open; it is settled as notarised direct download.) - Web: an opt-in GitHub Pages deploy from Publish · Web app
(
-f deploy=true). - One dispatch of Release · Train (all platforms) with
-f track=alphaputs iOS and Android on the same commit, which is the point of having a train at all.
- File the issue (or Epic + children if > 1 PR).
- Branch
feat/<slug>off master. - Write the failing test first.
- Model in
domain/(freezed), seam in the repository interface, Supabase impl indata/, provider, screen. - New strings → an ARB fragment per locale in
lib/l10n/_fragments/, thendart run tool/build_arb.dart && flutter gen-l10n. - New tables/RPCs → numbered migration with RLS + revokes and the six
system columns (
ensure_system_columns); harness it first — run the whole thing inside a rolled-back transaction against the hosted project beforeapply_migration. Update the fake repository to mirror the server contract. - Every functionality goes behind a
WorkspaceFeature, andweb/setup.htmlchanges in the same PR as any parameter. - Anything a user can see or configure gets a guide section with an
<!-- anchor: … -->, and the symbol beside it points there. -
flutter analyze --fatal-infos lib test tool && flutter testgreen locally, then PR with the template (What/Why/Testing/Completeness).
Developers
Administrators
- Configuring the space
- Configurer l'espace 🇫🇷
- Den Bereich einrichten 🇩🇪
- Configurar el espacio 🇪🇸
- Configurare lo spazio 🇮🇹
- The technical side
- Le côté technique 🇫🇷
- Die technische Seite 🇩🇪
- La parte técnica 🇪🇸
- La parte tecnica 🇮🇹
- Environments: dev and prod
- Environnements : dev et prod 🇫🇷
- Umgebungen: Dev und Prod 🇩🇪
- Entornos: dev y prod 🇪🇸
- Ambienti: dev e prod 🇮🇹
Users — on the guide site