Toutes les études
App iOS, web et backend

Klaaro Card

Un portefeuille iOS natif pour cartes de visite que tout le monde peut ouvrir sans l'app. L'essentiel du travail difficile était côté backend : la propriété sans compte, et une sécurité au niveau des lignes qui résiste aux requêtes directes sur l'API.

Rôle
PM et développeur principal (136 commits sur 149)
Période
mai 2026 – sept. 2026
Équipe
Équipe de 4 personnes, Apple Developer Academy
30+
bêta-testeurs avant le lancement sur l'App Store
26
migrations de base de données versionnées
SwiftSwiftUISupabasePostgreSQLEdge FunctionsSign in with AppleNext.jsCoreImage
Klaaro Card
01

Le produit

Klaaro Card est un portefeuille de cartes de contact professionnelles. Vous concevez votre carte, puis la partagez par QR code ou d'un simple geste. Le QR code contient un lien klaaro.cards, donc la personne qui le scanne n'a pas besoin de l'app : les Universal Links ouvrent l'app si elle est installée, et une page Next.js affiche la carte sinon.

J'étais PM et développeur principal d'une équipe de 4 personnes à l'Apple Developer Academy, sur l'app SwiftUI, les pages web et le backend Supabase.

02

La propriété sans compte

Personne ne veut créer un compte juste pour donner une carte de visite, donc il n'y a pas de connexion par défaut. - Chaque carte appartient à un jeton secret de l'appareil stocké dans le Trousseau, synchronisé via le Trousseau iCloud quand il est disponible. - Les politiques d'écriture de la sécurité au niveau des lignes de Postgres comparent ce jeton à un en-tête de la requête. - Se connecter avec Apple est optionnel et n'ajoute que la synchronisation. - À la réinstallation, l'app restaure la carte via une fonction de base de données qui compare le jeton et ne le renvoie jamais.

03

Le bug qui restaurait la carte d'un inconnu

Au départ, la restauration utilisait une simple requête. Les cartes avaient deux politiques de lecture : tout le monde peut lire les cartes publiques, et les propriétaires peuvent lire la leur avec le jeton. Postgres combine les politiques permissives avec OR, donc sur un appareil sans jeton correspondant, la requête renvoyait quand même toutes les cartes publiques, et l'app pouvait restaurer la carte d'un inconnu au hasard.

J'ai déplacé la restauration dans une fonction SECURITY DEFINER qui lit directement l'en-tête du jeton et ne renvoie que la carte correspondante, ou rien.

04

Colmater une fuite de jetons

La sécurité au niveau des lignes décide quelles lignes un rôle peut lire, pas quelles colonnes. Avec la clé publique de l'API, n'importe qui pouvait interroger la colonne du jeton d'appareil des cartes publiques, et ce jeton est exactement ce sur quoi s'appuie chaque politique d'écriture : il aurait permis de prendre le contrôle de la carte de quelqu'un. Les identifiants des propriétaires et les informations de paiement qu'ils avaient choisi de masquer étaient exposés de la même façon.

J'ai révoqué l'accès en lecture à toute la table et réaccordé uniquement les colonnes dont les clients publics ont besoin.

Ce correctif a cassé la synchronisation des cartes. Les upserts de l'app forçaient PostgREST à relire la colonne du jeton, ce que les nouveaux droits interdisent, donc chaque enregistrement échouait avec une 401. Réaccorder la colonne aurait rouvert la fuite. À la place, un trigger de la base remplit désormais le jeton à partir de l'en-tête de la requête, et l'app ne l'envoie plus dans le corps. Pour les clients publics, le jeton est désormais en écriture seule.

05

Ce que l'app ne doit pas faire

Les Edge Functions de Supabase gèrent les tâches qui ne doivent pas s'exécuter avec une clé publique : l'une envoie des notifications push quand quelqu'un reçoit une carte, l'autre supprime un compte et ses données.

06

Résultats

  • 30+ bêta-testeurs avant le lancement sur l'App Store.
  • Disponible sur l'App Store depuis septembre 2026.