Skip to content
Browse documentation
Decided

Architecture overview

How the native apps, Supabase and the website fit together in Incico v3.

Updated: 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

PieceTechnologyResponsibility
Android appKotlin, Jetpack Compose, Material 3Phone and tablet UI, local cache, device-contacts sync, Google Contacts sync
iOS appSwift, SwiftUISame, with Contacts framework and the same Google Contacts sync
DatabaseSupabase Postgres 17Profiles, details, circles, connections, invitations, snapshots, notifications, devices
AuthSupabase AuthE-mail + password, magic link, Google, Apple; JWT sessions with refresh-token rotation
StorageSupabase StorageProfile photos in a private bucket
Edge functionsDeno (TypeScript)Push fan-out, account deletion, data export, e-mail templates, rate-limited search
RealtimeSupabase RealtimePer-user notification stream while the app is in the foreground
WebsiteAstro on NetlifyMarketing, documentation (this site), legal pages, account-deletion page

Principles that drive the shape

  1. The database enforces the rules. Who may see which detail is decided in Postgres with row-level security and security definer functions, 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.
  2. 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 AccessToken and RefreshToken in plaintext columns; v3 has no such columns.
  3. Snapshots, not live joins. Each connection carries a snapshot JSON 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’s DetailsJson and v2’s hash fields but moves the computation into the database.
  4. 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.
  5. 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

EnvironmentSupabase projectPurpose
localsupabase start (Docker)Unit and integration tests for SQL, RLS and edge functions
stagingincico-stagingTestFlight and Play internal testing builds
productionincicoStore 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.