Naar de inhoud
Documentatie doorbladeren

De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.

Besloten

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

ItemChoice
LanguageKotlin 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)
UIJetpack Compose, Material 3, Compose BOM 2025.12.00
Min / target SDK27 / 36 (Saymail’s values; confirmed, not revisited)
ArchitectureMVVM with unidirectional state: XxxRoute.kt (nav entry and VM wiring), XxxScreen.kt (stateless composable), XxxViewModel.kt exposing StateFlow
DINo 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).
Navigationnavigation-compose 2.8.5, four tab destinations plus pushed routes in one Routes object; TabletShell for two-pane
Backend clientsupabase-kt 3.8.0 (Auth, PostgREST, Realtime modules) over Ktor 3.5.1
Local cacheRoom 2.7.1 with exported schemas, one entity per server table plus SyncCursor; destructive migration allowed only before 1.0
Background workWorkManager for periodic external-contact sync (15 min minimum, network-constrained) and for retrying failed uploads — arrives in phase 4; not built in phase 2
PushFirebase Cloud Messaging, initialised manually from build-time values — arrives in phase 4; not built in phase 2
ImagesCoil — arrives in phase 3; not built in phase 2
Phone numberslibphonenumber-android
ContactsContactsContract via a small DeviceContactsGateway
Google ContactsPeople API over Ktor with tokens from AppAuth (PKCE, public client)
Sign-inandroidx.credentials 1.5.0 + googleid 1.1.1 for native Google sign-in via Credential Manager
Web pagesandroidx.browser Custom Tabs for the FAQ, What’s new, Terms and Privacy (Settings → Help and About)
LicencesAboutLibraries 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)
LoggingOne 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 crashesA hand-written PostHog /batch client (Telemetry), no vendor SDK (A50); crash reports through the same client (A89)
TestsJUnit 4, Truth, MockK, Turbine, Robolectric, Room testing, MockWebServer; Compose UI tests with semantics only

iOS

ItemChoice
LanguageSwift 6 toolchain in Swift 5 language mode, SWIFT_STRICT_CONCURRENCY = complete with SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
UISwiftUI, iOS 17 minimum (deployment target 17.0; confirmed, not revisited)
ArchitectureFeature 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.
NavigationTabView + NavigationStack per tab; SplitShell for two-pane on iPad
Backend clientsupabase-swift 2.55.2 (the one third-party dependency, via Swift Package Manager, pinned)
Local cacheSQLite 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 workBGAppRefreshTask for external-contact sync — arrives in phase 4; not built in phase 2
PushAPNs 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
ContactsContacts framework via DeviceContactsGateway
Google ContactsPeople API over URLSession with tokens from ASWebAuthenticationSession (PKCE, public iOS client)
Phone numbersPhoneNumberKit 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
TestsXCTest, protocol-based fakes, no mocking framework

Shared

  • mobile/design/tokens.json (design tokens) and mobile/design/i18n/en.json + nl.json (76 canonical keys, typed into string tables on both platforms) are the contract, checked by tools/tokens-check.mjs and tools/i18n-diff.mjs; this documentation site is the specification itself, not a separate SPECIFICATION.md file.
  • 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)

  1. 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: a feature/contacts/ContactsRepository.kt lives with its feature, not in a data/ folder.
  2. 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.
  3. Create dependencies close to use. No constructing objects at the top of a method that might early-return.
  4. 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.
  5. Untrusted input at every boundary. Trim once, reject empty and whitespace-only, cap lengths, validate lists, before binding. The server re-validates with constraints.
  6. No dead code, no commented-out code, orphan functions deleted rather than kept.
  7. Secrets only in the secure store, identifiers may be constants; one AppConfig reads build values.
  8. Document the decision in the code when a choice is non-obvious, with the date and the spec section.
  9. Tests: Arrange-Act-Assert with comments, Action_WithCondition_ShouldResult naming, one test class per source class, both success and failure paths, builders in a Builders/ folder beside the tests, test files under 400 lines.
  10. Database rules for the local cache: WAL, busy_timeout, never touch the database from UI code, tight transactions, FTS5 for contact search, no LIKE on 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

ToolUse
ktlint + detektKotlin style and complexity (file length, method length rules set to the guideline values)
SwiftFormat + SwiftLintSwift equivalents with file_length: 300, function_body_length: 50
gitleakspre-commit and CI
tools/i18n-diff.mjs, tools/vectors-check.mjsparity checks
PlantUML in Dockerarchitecture diagrams under docs/architecture/*.puml, ASCII-only