Klaaro Card
A native iOS wallet for business cards that anyone can open without the app. Most of the hard work was in the backend: ownership without accounts, and row-level security that holds up against raw API queries.

The product
Klaaro Card is a wallet for professional contact cards. You design your own card, then share it by QR code or by tap. The QR code encodes a klaaro.cards link, so the person scanning it does not need the app: Universal Links open the app when it is installed, and a Next.js page shows the card when it is not.
I was PM and lead developer on a 4-person team at the Apple Developer Academy, working across the SwiftUI app, the web pages and the Supabase backend.
Ownership without an account
Nobody wants to create an account just to hand out a business card, so there is no login by default. - Each card is owned by a secret device token stored in the Keychain, synced through iCloud Keychain when available. - Write policies in Postgres row-level security compare that token with a request header. - Sign in with Apple is optional and only adds sync. - On reinstall, the app restores the card through a database function that matches the token and never returns it.
The bug that restored a stranger's card
Restore first used a plain query. Cards had two read policies: anyone can read public cards, and owners can read their own card by token. Postgres combines permissive policies with OR, so on a device with no matching token the query still matched every public card, and the app could restore a random stranger's card.
I moved restore into a SECURITY DEFINER function that reads the token header directly and returns only the card that matches, or nothing.
Closing a token leak
Row-level security decides which rows a role can read, not which columns. With the public API key, anyone could query the device token column of public cards, and that token is exactly what every write policy trusts, so it would have allowed taking over someone's card. Owner IDs and payment details that owners had chosen to hide were exposed the same way.
I revoked table-wide read access and granted back only the columns public clients need.
That fix broke card sync. The app's upserts made PostgREST read the token column back, which the new grants forbid, so every save failed with a 401. Granting the column again would have reopened the leak. Instead, a database trigger now fills the token from the request header, and the app stopped sending it in the request body. For public clients, the token is now write-only.
Work the app should not do
Supabase Edge Functions handle the jobs that should not run with a public key: one sends push notifications when someone receives a card, and another deletes an account and its data.
Results
- 30+ beta users before the App Store launch.
- Live on the App Store since September 2026.