De documentatie is alleen in het Engels geschreven. De rest van de site is ook in het Nederlands beschikbaar.
Architecture overview
How the native apps, Supabase and the website fit together in Incico v3.
Bijgewerkt: 12 September 2026
Incico v3 is two native mobile apps talking to one Supabase project. There is no custom API server, no identity server and no background worker fleet. Everything v1 ran on Azure (an ASP.NET API, Duende IdentityServer, a service bus, blob storage, an OAuth bridge) collapses into Postgres, Supabase Auth, Supabase Storage and a small set of edge functions.
The pieces
| Piece | Technology | Responsibility |
|---|---|---|
| Android app | Kotlin, Jetpack Compose, Material 3 | Phone and tablet UI, local cache, device-contacts sync, Google Contacts sync |
| iOS app | Swift, SwiftUI | Same, with Contacts framework and the same Google Contacts sync |
| Database | Supabase Postgres 17 | Profiles, details, circles, connections, invitations, snapshots, notifications, devices |
| Auth | Supabase Auth | E-mail + password, magic link, Google, Apple; JWT sessions with refresh-token rotation |
| Storage | Supabase Storage | Profile photos in a private bucket |
| Edge functions | Deno (TypeScript) | Push fan-out, account deletion, data export, e-mail templates, rate-limited search |
| Realtime | Supabase Realtime | Per-user notification stream while the app is in the foreground |
| Website | Astro on Netlify | Marketing, documentation (this site), legal pages, account-deletion page |
Principles that drive the shape
- The database enforces the rules. Who may see which detail is decided in Postgres with row-level security and
security definerfunctions, never in the app. A rooted phone or a modified client cannot see more than the server allows. In v1 the sharing-circle filter lived in the API layer; in v2 the mock API returned everything and the client filtered. Both are gone. - The server never holds third-party credentials. Google Contacts and device contacts are synced from the phone. OAuth tokens for Google live in the Keystore or Keychain, never in a table. v1 stored
AccessTokenandRefreshTokenin plaintext columns; v3 has no such columns. - Snapshots, not live joins. Each connection carries a
snapshotJSON of what the other party is allowed to see, plus a hash. When someone edits a detail, a trigger recomputes the snapshots of every connection in the affected circles. Contact lists render from snapshots, external sync diffs by hash, and notifications are generated from the same change. This mirrors v1’sDetailsJsonand v2’shashfields but moves the computation into the database. - Native, twin apps from one specification. Like Saymail, both apps implement one platform-agnostic specification and one visual design; only mechanics differ (gestures, back navigation, push transport, secure storage). See Tech stack and coding guidelines.
- Offline-readable, online-editable. The app keeps a local cache of the user’s own profile and all connection snapshots so contacts remain readable without a network. Edits require a connection and are applied optimistically with rollback on failure.
Request paths
Phone ──(supabase-kt / supabase-swift, JWT)──► PostgREST ──► Postgres (RLS + RPC)
Phone ──(JWT)──► Edge function (search, delete-account, export, photo-url)
Postgres trigger ──► notifications row ──► DB webhook ──► push-fanout edge fn ──► FCM / APNs ──► Phone
Phone ◄──(Realtime channel: notifications where user_id = me)── Postgres
Phone ──(OAuth PKCE, on device)──► Google People API (contacts sync, never via server)
Phone ──(Contacts framework / ContactsContract)──► device address book
Environments
| Environment | Supabase project | Purpose |
|---|---|---|
| local | supabase start (Docker) | Unit and integration tests for SQL, RLS and edge functions |
| staging | incico-staging | TestFlight and Play internal testing builds |
| production | incico | Store releases |
Each app build carries the URL and anon key of exactly one environment, injected at build time the way Saymail injects SUPABASE_URL and SUPABASE_ANON_KEY (Gradle property or repo-root .env on Android; a generated AppConfig on iOS). The anon key is a public identifier scoped by RLS; there is no confidential client secret in either app.
What is deliberately not there
- No web client for the app itself. The website hosts only an account-deletion and data-export page, which both stores require.
- No ads SDK at launch (Q4, decided).
- No custom URL scheme for authentication. Magic links and OAuth callbacks use verified universal links / app links on
incico.eu, which cannot be hijacked by another app. - No server-side storage of external OAuth tokens.
- No SMS gateway at launch: phone numbers are shown as “unverified” until a later release adds verification.