De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.
Tech stack and coding guidelines
Languages, frameworks, project structure and the coding rules both apps follow, taken from the Saymail mobile apps.
Bijgewerkt: 18 September 2026
Incico v3 is built the way Saymail’s mobile apps are built: two fully native apps, one platform-agnostic specification, one visual design, no cross-platform framework. The differences from Saymail are only where Incico’s problem is different (a server-backed shared dataset instead of a local-first mail store).
Android
| Item | Choice |
|---|---|
| Language | Kotlin 2.4.20 (moved up from the spec’s 2.2.10: supabase-kt 3.8.0 ships Kotlin 2.4 metadata; KSP moved with it to 2.3.12, which no longer carries a Kotlin-version prefix) |
| UI | Jetpack Compose, Material 3, Compose BOM 2025.12.00 |
| Min / target SDK | 27 / 36 (Saymail’s values; confirmed, not revisited) |
| Architecture | MVVM with unidirectional state: XxxRoute.kt (nav entry and VM wiring), XxxScreen.kt (stateless composable), XxxViewModel.kt exposing StateFlow |
| DI | No framework. A hand-written AppContainer created in Application.onCreate, exposed via LocalAppContainer, with the 8-line appViewModel { } helper from Saymail. Kept small by grouping into AuthContainer, ProfileContainer, ContactsContainer, SyncContainer (Saymail’s single 1 366-line container is the anti-pattern to avoid). |
| Navigation | navigation-compose 2.8.5, four tab destinations plus pushed routes in one Routes object; TabletShell for two-pane |
| Backend client | supabase-kt 3.8.0 (Auth, PostgREST, Realtime modules) over Ktor 3.5.1 |
| Local cache | Room 2.7.1 with exported schemas, one entity per server table plus SyncCursor; destructive migration allowed only before 1.0 |
| Background work | WorkManager for periodic external-contact sync (15 min minimum, network-constrained) and for retrying failed uploads — arrives in phase 4; not built in phase 2 |
| Push | Firebase Cloud Messaging, initialised manually from build-time values — arrives in phase 4; not built in phase 2 |
| Images | Coil — arrives in phase 3; not built in phase 2 |
| Phone numbers | libphonenumber-android |
| Contacts | ContactsContract via a small DeviceContactsGateway |
| Google Contacts | People API over Ktor with tokens from AppAuth (PKCE, public client) |
| Sign-in | androidx.credentials 1.5.0 + googleid 1.1.1 for native Google sign-in via Credential Manager |
| Web pages | androidx.browser Custom Tabs for the FAQ, What’s new, Terms and Privacy (Settings → Help and About) |
| Licences | AboutLibraries Gradle plugin 15.2.0 (com.mikepenz.aboutlibraries.plugin.android) generates raw/aboutlibraries per variant; no AboutLibraries runtime or UI library, the app renders the list itself (A90) |
| Logging | One AppLog facade over logcat and the diagnostics log; android.util.Log is called nowhere else, and tools/log-check.mjs fails CI on a sensitive name in a log template (A88) |
| Analytics and crashes | A hand-written PostHog /batch client (Telemetry), no vendor SDK (A50); crash reports through the same client (A89) |
| Tests | JUnit 4, Truth, MockK, Turbine, Robolectric, Room testing, MockWebServer; Compose UI tests with semantics only |
iOS
| Item | Choice |
|---|---|
| Language | Swift 6 toolchain in Swift 5 language mode, SWIFT_STRICT_CONCURRENCY = complete with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor |
| UI | SwiftUI, iOS 17 minimum (deployment target 17.0; confirmed, not revisited) |
| Architecture | Feature models with @Observable, one per screen or flow, created by a small AppContainer injected through the environment. The single 114 KB Store that Saymail iOS grew into is explicitly not repeated: no type over 300 lines. |
| Navigation | TabView + NavigationStack per tab; SplitShell for two-pane on iPad |
| Backend client | supabase-swift 2.55.2 (the one third-party dependency, via Swift Package Manager, pinned) |
| Local cache | SQLite via SQLite3 in the document-store layout Saymail iOS uses (fixed indexed columns plus a JSON blob), WAL, one connection behind a serial queue |
| Background work | BGAppRefreshTask for external-contact sync — arrives in phase 4; not built in phase 2 |
| Push | APNs via UIApplicationDelegateAdaptor, one alert push per notification carrying content-available so the cache refreshes before the tap (A57) — arrives in phase 4; not built in phase 2 |
| Contacts | Contacts framework via DeviceContactsGateway |
| Google Contacts | People API over URLSession with tokens from ASWebAuthenticationSession (PKCE, public iOS client) |
| Phone numbers | PhoneNumberKit is avoided to keep dependencies minimal; a vendored subset of libphonenumber metadata for parsing and formatting, generated at build time, shared with Android’s expectations by golden tests |
| Tests | XCTest, protocol-based fakes, no mocking framework |
Shared
mobile/design/tokens.json(design tokens) andmobile/design/i18n/en.json+nl.json(76 canonical keys, typed into string tables on both platforms) are the contract, checked bytools/tokens-check.mjsandtools/i18n-diff.mjs; this documentation site is the specification itself, not a separateSPECIFICATION.mdfile.- Golden vectors shared from
docs/vectors/*.json(phone formatting, address formatting, snapshot hashing, circle-dot rendering order) start in phase 3; phase 2 has nothing to pin yet. - The same i18n key set, checked by
tools/i18n-diff.mjs. - The same screen names, test tags and analytics event names.
Coding guidelines (from Saymail’s docs/coding-guidelines.md, applied)
- Screaming architecture. Folders by feature and use case (
feature/contacts/invite/,feature/profile/addresses/), never by technical role. Shared code bubbles up one level only when two use cases need it. Incico applies the rule strictly, including for data access: afeature/contacts/ContactsRepository.ktlives with its feature, not in adata/folder. - Single responsibility, ~300-line files, ~50-line methods. Orchestration methods read like an outline of well-named private steps. A file over 300 lines is refactored, not split into partials/extensions.
- Create dependencies close to use. No constructing objects at the top of a method that might early-return.
- Error boundaries at the edge. Calls to Supabase, Google or the OS address book are wrapped once at the boundary; internal methods throw. Errors are typed:
Invalid(message)surfaced verbatim,NotFound(entity id),Network,Unauthorized(forces re-login),Server(redacted, logged). No swallowed results;runCatching/try?without logging is a review failure. - Untrusted input at every boundary. Trim once, reject empty and whitespace-only, cap lengths, validate lists, before binding. The server re-validates with constraints.
- No dead code, no commented-out code, orphan functions deleted rather than kept.
- Secrets only in the secure store, identifiers may be constants; one
AppConfigreads build values. - Document the decision in the code when a choice is non-obvious, with the date and the spec section.
- Tests: Arrange-Act-Assert with comments,
Action_WithCondition_ShouldResultnaming, one test class per source class, both success and failure paths, builders in aBuilders/folder beside the tests, test files under 400 lines. - Database rules for the local cache: WAL,
busy_timeout, never touch the database from UI code, tight transactions, FTS5 for contact search, noLIKEon JSON.
Website
Astro 7, Tailwind v4 tokens, MDX docs collection, hand-rolled en/nl i18n, Netlify. See the site’s own README in web/.
Repository tooling
| Tool | Use |
|---|---|
ktlint + detekt | Kotlin style and complexity (file length, method length rules set to the guideline values) |
SwiftFormat + SwiftLint | Swift equivalents with file_length: 300, function_body_length: 50 |
gitleaks | pre-commit and CI |
tools/i18n-diff.mjs, tools/vectors-check.mjs | parity checks |
| PlantUML in Docker | architecture diagrams under docs/architecture/*.puml, ASCII-only |