Inspiration

Every networking event ends the same way. You come home with a pocket of paper cards, or a phone full of contacts you accepted but cannot place. Two weeks later you are looking at a name and you have no idea who the person was, what you talked about, or what you promised to send them.

The problem was never collecting contacts. Collecting is easy, and every app already solves it. The problem is that the context is gone. The handshake, the room, the thing they were excited about, the follow up you meant to send: all of it evaporates within days, and what survives is a name attached to nothing.

So I built Kineo around a deliberate constraint that no other contact app accepts: you cannot add someone unless you actually met them. There is no QR code to scan from across a room, no contact list to import, no connection request to accept from a stranger. You share a four digit code out loud and both phones confirm you are physically together. Every card in Kineo is proof of a real meeting.

The constraint is the product. It makes the collection small, honest, and worth remembering, which is exactly what makes the AI context on top of it useful instead of noise.

What it does

Exchange in person. Open Exchange, say a four digit code out loud, the other person enters it, and you both tap. The server checks that both phones are within a mile of each other before it writes anything. Rooms expire after two minutes and codes are single use, so a code overheard or screenshotted is worthless a few minutes later.

AI designed cards. Describe the impression you want to make and OpenAI's gpt-image-2 generates a card background around it: colors, texture, and mood that look like you rather than like a template. You can hold several cards for the different rooms you walk into.

Context that survives. Right after a meeting, a short chat style AI session asks what you discussed, what you promised, and what you had in common. Claude turns the answers into structured context: topics, follow ups, shared interests, relationship strength. Later you can ask about anyone and get the answer back in plain language.

A card stack you can actually move through. Connections live in a swipeable stack with an A to Z index down the edge. The index sits on the left or the right depending on which thumb you told the app you use, and the cards shift to the opposite side so your hand never covers what you are reading.

The rest of what shipped. Private notes and tags, achievements, an in app Ideas board with reporting and moderation, a constellation view of how your connections relate, and push notifications that nudge you to capture context while it is still fresh.

Free covers two cards and the full in person exchange. Kineo Pro adds up to five cards, ten AI card designs a month, AI context on every connection, and priority support.

How I built it

Client. React Native and Expo SDK 54, React Native 0.81, React 19, TypeScript in strict mode. Reanimated drives the card stack and the index animation. iPhone only for 1.0.

Backend. Supabase end to end: Postgres with row level security on every table, Auth, Realtime, Storage, and eleven Edge Functions. The app is layered Screens, Hooks, Services, Supabase, and only the Supabase service implementations import the client, so the rest of the app has no idea what the backend is.

The exchange. Room state rides Supabase Realtime so both phones see "ready" at the same moment. The actual exchange runs through a single atomic Postgres function, attempt_exchange, which validates proximity and writes both connection rows in one transaction. That matters more than it sounds: two people tap at nearly the same instant, and a naive implementation produces duplicates, or one person stuck forever on "waiting for the other person."

Monetization with RevenueCat. react-native-purchases with one entitlement, pro, granted by all three products (monthly, yearly, lifetime), so every gate in the app is a single question: is pro active. The SDK is configured once at launch and is safe by default. If the key is missing or the native module is not there, it no ops and the app boots with billing disabled rather than crashing.

The piece I am most glad I built properly is the server side of it. profiles.subscription_tier is locked against client writes at the Postgres level, and the only writer is a revenuecat-webhook Edge Function. It maps the full event vocabulary in both directions, and the subtleties are where the bugs would have been:

  • BILLING_ISSUE does not revoke. RevenueCat keeps the entitlement alive through the billing retry grace period, so stripping access on a billing hiccup would punish a paying user for a declined card.
  • CANCELLATION does not revoke either. Cancelling is auto renew off, and access runs to EXPIRATION. Only an actual refund revokes immediately.
  • An existing lifetime is never downgraded by an unrelated subscription event.
  • Every event id is recorded, so duplicate deliveries are acknowledged and skipped.
  • The fifty seat Lifetime cap is consumed through an atomic check and set function, so two simultaneous purchases cannot both take the last seat.

AI. OpenAI gpt-image-2 for card art, Claude for the context interviews and the plain language answers, both behind Edge Functions so no key ever reaches the device.

Analytics. PostHog with session replay, which is how I found several onboarding drop offs I would never have guessed at.

Challenges I ran into

Apple rejected build 7 under Guideline 4, and the cause was a workaround I had written down. Sign in with Apple requested the full name scope and then threw the result away, passing only the identity token onward. That token carries the email but not the name, so the new user trigger fell back to seeding the display name with the email address, which under Hide My Email is a @privaterelay.appleid.com string. Onboarding then noticed "display name equals email", blanked the field, and asked for a name Apple had already handed me. That is exactly the violation: do not ask for what Authentication Services already gave you.

The reason two rounds of device testing never caught it is the part worth keeping: Apple returns fullName only on a user's first authorization per Apple ID, per app. Every sign in after that returns null. Once anyone had tapped through once, correct and broken behavior were indistinguishable, because the app asked for a name either way. Testing it again at all means going into iOS Settings and revoking the app's access to your Apple ID first.

The fix persists the name alongside the token, and only ever fills a name that is empty or still equal to the email, so a name the user chose is never overwritten by a later sign in that sends nothing. Build 8 was approved.

Prices that disagreed with themselves. Monthly was hardcoded in three screens and had drifted ($4.49 in one place, $4.99 in another, a lifetime that was $199 in one screen and $49.99 in another). Worse, the yearly App Store description claimed "Save 30%" when the real figure against twelve monthly payments is 25%, and the fallback introductory price was a naive half of monthly, $2.24, when the configured offer is $2.29. A fallback that undercuts the real charge is precisely the price inaccuracy Apple looks for. All of it now comes from one pricing module, with RevenueCat Offerings as the live source and those constants only as the offline fallback.

I shipped Expo's placeholder splash screen. The icon had been replaced, so "the assets are done" felt true. splash-icon.png was still the grey template image, byte identical to the file next to it. The check that would have caught it is one line: md5 assets/*.png, and two files sharing a hash means neither is yours. Config that references an asset is not the same as an asset that is yours.

Proving the exchange works takes two phones and another person. No simulator reproduces it. The race condition where both people tap at the same instant only shows up when two humans are actually standing there tapping at the same instant.

Accomplishments that I am proud of

Kineo 1.0 is live on the App Store, approved on 23 September 2026, built solo.

Beyond shipping it: the exchange holds up under simultaneous taps, the subscription state is server authoritative and survives refunds, lapses, reinstalls, and sign outs, and the whole thing degrades gracefully when a key or a native module is missing instead of crashing. 386 tests across 35 suites cover the paths manual testing physically cannot repeat, including four written specifically because they failed against the shipped build 7 behavior before I kept them.

What I learned

When a flow behaves differently on first use than on every later use, the first use path needs an automated test, because manual testing will only ever see it once. This is the single most useful thing I took out of the rejection, and it generalizes well past Sign in with Apple.

Put the money logic on the server. Letting the client write its own subscription tier would have been quicker and would have been wrong. One locked column, one webhook writer, idempotent by event id, and the entire category of "user is premium on this device but not that one" disappears.

Read the whole event vocabulary before mapping it. BILLING_ISSUE and CANCELLATION both look like "take it away" and neither is. RevenueCat's docs are explicit about this, and getting it wrong would have quietly revoked access from paying users at the worst possible moment.

Write down the ceiling, not just the feature. Room codes are four digits, and the graph query is quadratic. Both are completely fine at today's size and both have a size where they are not, so each one now has the number to watch written next to it.

What's next for Kineo

Expanding beyond the US only launch, Android, contact fields (email, phone, LinkedIn, website) carried across the exchange, a web view of a card so the person you met does not need the app installed to see it, smart follow up reminders driven by the context already captured, and growing the constellation view into real introduction paths through the people you know.

Built With

Share this project:

Updates

Submission history