FINAL PLAN · v3.6

PinBook 2

Cross-platform location address book — React Native + Expo · iOS + Android.

Expo Clerk auth Convex backend WhatsApp → Save Rapido Accounts (day 1)

The project in one minute

PinBook is a location address book for your phone. Someone sends you an address on WhatsApp — instead of scrolling back through old chats weeks later to find it, you save it once: labeled, searchable, favorite-able. PinBook 2 is the same idea, rebuilt to work on both iPhone and Android, with sharing that reaches anyone — even someone who doesn't have the app at all.

How it flows

SaveOrganizeOpenShare

Save — a location arrives on WhatsApp or Google Maps → share it to PinBook → it's saved with a name you choose.

Organize — search, favorites, categories, notes. Your places, your way.

Open — tap "Open With" → Google Maps navigation, or straight into Rapido as the destination.

Share — to a friend in-app (they get a notification), or to anyone by number (they get a link that opens in Maps).

Meet the users — four little stories

Read these four stories — together they explain the whole product. No tech needed.

Story 1 · Ravi saves a cafe 🧍☕

Ravi's cousin Meera messages him a location on WhatsApp: "Let's meet here on Saturday." Ravi taps Share → PinBook and names it "The Blue Door Cafe". Done — 5 seconds.

Two weeks later he wants to go back. He opens PinBook, types "cafe" in search — it's right there, with its address. No scrolling through old chats, no "which message was it?"

WhatsApp locationShare to PinBookNamed & savedFound by search later

Shows: saving, labeling, search — the address-book core.

Story 2 · Ravi rides home 🛺

Saturday arrives. Ravi is at the cafe and books an auto. The driver asks, "Where to, sir?" Ravi opens PinBook → taps the saved place → Open With → picks Rapido from the list.

The cafe's location is now the ride's destination. No typing the address, no spelling it out, no "turn left at the big tree".

Saved placeOpen WithRapidoDestination set

Shows: one tap from your list into any maps/ride app.

Story 3 · Priya gets it too 🧍‍♀️📲

Ravi wants Priya to join them. Both have PinBook. Ravi opens the cafe → Send to Priya.

Priya's phone pings: "Ravi shared The Blue Door Cafe with you." She opens it, sees it on a map, and taps Save to my places — now it's in her list too, for next time.

Ravi taps SendPriya's phone pingsShe views the placeSaves her own copy

Shows: in-app sharing between friends — instant, with a notification.

Story 4 · The auto driver 🛵🔗

Ravi is at his cousin Meera's flat in Jubilee Hills and needs an auto. The driver doesn't have PinBook — and doesn't need it. Ravi taps Share by number, types the driver's number, sends.

The driver's WhatsApp pings with a link. He opens it → a simple page shows "Meera's flat — 7, Jubilee Hills, Hyderabad" with a map → he taps Open in Google Maps and drives.

No app download. No account. No friendship needed. And the link stops working after an hour.

Type numberWhatsApp linkWeb page with mapOpen in Google Maps

Shows: sharing with ANYONE — even people without the app.

1

Final stack — locked

LayerChoiceStatus
FrameworkExpo (React Native)✓ Locked
NavigationReact Navigation✓ Locked
Local storageExpo SQLite / MMKV✓ Locked
Auth (day 1)Clerk — Gmail / email OTP now, phone OTP later✓ Locked
Backend (day 1)Convex — reactive DB + TypeScript functions✓ Locked
Push (P2)Expo Push Service✓ Locked
Maps (V3)react-native-maps⚠ Pending (decision 3)
Share targetShare Menu (Android) + Swift ext (iOS)✓ Locked
Why this combo: Clerk = best-in-class managed auth (ready-made signup UI). Convex = TypeScript end-to-end, realtime by default, security enforced in functions. Both free to start. Supabase, Firebase, Appwrite, Nhost, PocketBase → evaluated and not chosen.
2

Supporting services

TierServiceJobCost
Day 1EAS (Expo)Cloud builds, store submission, OTA updates, code signingFree
Day 1Expo PushPush notifications, no FCM/APNs key jugglingFree
Day 1SentryCrash & error monitoringFree tier
Day 1PostHogAnalytics + feature flagsFree tier
LaunchVercelLanding page + universal-link filesFree
LaunchInfisicalSecret keys vault (Clerk/Convex keys)Free
LaunchGitHub ActionsCI — tests + auto buildsFree

Deliberately not added: React Query (Convex does it), Firebase (replaced), second auth service (Clerk covers it).

3

All features

F1 · Save location from WhatsApp CoreiOS: native ext

Share from WhatsApp → PinBook in share sheet → parse → save.

WhatsApp shareShare sheetPinBookParse geo / URLSave + label

F2 · Save location from Google Maps CoreiOS: native ext

Same share-target mechanism; resolves short links (maps.app.goo.gl) to coordinates.

F3 · Open in Rapido as destination Core

"Open With" shares a clean geo:lat,lng URI → Rapido & maps apps appear as destination options. Proven approach, via Linking.

F4 · Accounts — login with phone or Gmail Day 1Clerk

Sign up with phone + OTP (SMS auth = Clerk Pro, $20/mo) or email OTP / Google login (free). Profile: name, number, avatar. No passwords.

F5 · Send to contacts who have the app V2Convex

Sync contacts → match on hashed phone numbers → see friends on PinBook → send saved locations in-app with push.

Your appConvexExpo PushFriend's app

F6 · Real map preview V3

react-native-maps pin preview on detail + add screens (replaces mock box).

F7 · Search · favorites · categories · notes Core

Carried from v1, plus dark mode, a proper "no results" empty state, and user-defined categories (home/work/family are just suggestions).

Onboarding — one guided nudge Core

After login, one card: "PinBook works from WhatsApp — try it now: open WhatsApp → share any location → pick PinBook." That's the whole onboarding. One moment, one nudge.

Removed from plan: "Send to Dad via WhatsApp" (old F4) — dropped by decision. Sharing happens in-app via F5 instead.
4

Security model

Convex — function-based access control No RLS needed

Client can only call your functions — never the database directly. Every function checks ctx.auth (who is calling) before touching data.

getMyLocations() → only MY locations
getSharedWithMe() → only shared with me
sendLocation(to) → logged in? owner? recipient exists?

Why safer than RLS for us: there's no "query the table directly" path to accidentally leave open. Follow the docs' security checklist function-by-function.

Extra layers

· Clerk bot protection — enable in dashboard (blocks scripted signup abuse).
· EAS code signing — keystores/provisioning stored encrypted.
· Sentry alerts — notified on crash or suspicious spike.
· Convex schema validation — typed, validated data.
· Hashed phone matching — no raw numbers stored for contact matching.

5

Costs — before you pay

ServiceFree tierWhen you'd pay
Clerk50K users/app, unlimited apps$20/mo — SMS/phone OTP, MFA, remove branding
Convex$0 — storage, search, crons, auth$25/dev/mo — backups, custom domain, SOC2
EAS / Push / Sentry / PostHog / Vercel / Infisical / CI$0At scale
Launch cost: ₹0. First likely bill: Clerk Pro $20/mo when phone-OTP becomes essential. Fallback if you want phone OTP free: Appwrite (built-in OTP, free tier) — the one alternative worth re-checking if that becomes a hard requirement.
6

Database schema — storage diagram

Convex document database. 6 collections. Every document gets _id + _creationTime automatically. 🔑 PK · 🔒 UNIQUE · ⚡ INDEXED · ⚠ REQUIRED

RELATIONSHIP MAPhow collections connect
User1 ── Nlocations(ownerId)
User1 ── Ncontacts(ownerId)
contacts.phoneHashN ── 1users.phoneHash(match → connection)
UserN ── NUservia connections
sharesN ── 1locations(locationId)
sharesN ── 1User (sender)(fromUserId)
sharesN ── 1User (receiver)(toUserId)
User1 ── Ndevices (push tokens)(userId)
locationsCORE
_ididPKauto
ownerIdidREQIDX→ users._id · who owns this place
labelstringREQdisplay name, e.g. "Home"
addressstring?street address if available
latitudenumber?parsed from link / geo URI
longitudenumber?parsed from link / geo URI
uristringoriginal shared link (maps.app.goo.gl…)
categorystringIDXfree text · suggested: home, work, family, other
notesstring?user's own note
isFavoritebooldefault false
sourceenumwhatsapp · google_maps · manual · received
savedAtdateIDXsort key for lists
1 ── N · ownerId
usersV2 · profile
_ididPKauto
clerkUserIdstringUNIQfrom Clerk · links auth → profile
phoneHashstringUNIQIDXhashed phone — contact matching
displayNamestringREQshown to friends
avatarUrlstring?Convex file storage
createdAtdateauto via _creationTime
1 ── N · ownerId
contactsV2 · synced phonebook
_ididPKauto
ownerIdidREQIDX→ users._id · whose phonebook
contactNamestringas stored on device
contactPhoneHashstringIDXhashed — never raw numbers
matchedUserIdid?IDX→ users._id when hash matches
createdAtdateauto
hash match → connection
connectionsV2 · friendships
_ididPKauto
userAIdidREQIDX→ users._id (lower id first)
userBIdidREQIDX→ users._id
statusenumpending · accepted · blocked
createdAtdateauto
N ── 1 · locationId / sender / receiver
sharesV2 · sent locations
_ididPKauto
locationIdidREQIDX→ locations._id · what was sent
fromUserIdidREQIDX→ users._id · sender
toUserIdidREQIDX→ users._id · receiver
messagestring?optional note along with location
sentAtdateauto
seenAtdate?null until recipient opens it
1 ── N · userId
devicesV2 · push targets
_ididPKauto
userIdidREQIDX→ users._id
pushTokenstringUNIQExpo push token
platformenumios · android
lastActiveAtdateprune dead tokens
PK — primary key UNIQ — unique IDX — indexed REQ — required ? — optional
Design notes: ① Phones stored ONLY as hashes — matching is privacy-safe. ② connections keeps pairs ordered (userA < userB) so each friendship is one row. ③ A share references a location, but the receiver sees a copy in their UI — the location itself stays private to its owner. ④ Push tokens pruned via lastActiveAt. ⑤ phoneHash is a salted HMAC-SHA256 (server secret) — raw hashes of phone numbers can be brute-forced; salted ones can't.

How the data connects — in plain words

· A location belongs to one user. Every saved place carries an ownerId — "whose list is this in?". A user can have many locations; a location has exactly one owner. That's the 1─N.

· Your phonebook becomes matching data. When you sync contacts, each number is hashed and stored in contacts. If a hash matches a users.phoneHash — that contact is a PinBook user! No raw numbers ever stored.

· Matches become friendships. A match doesn't auto-connect you — it creates a connection (pending → accepted). Only accepted connections can share. Two users, one row, ordered so it's unique both ways.

· Shares are the bridge. When you send a location, a share row records: which location, from whom, to whom, when, and whether it's been seen. The location itself never leaves your list — the receiver gets a copy via the share.

· Devices carry the alert. Each of your devices registers an Expo push token. When a share arrives, the app looks up the receiver's devices and pings them.

Worked example — Ravi & Priya 🧍→📲→🧍‍♀️

Step 1 · Ravi saves a place from WhatsApp.
Ravi taps "Share" on a WhatsApp location → PinBook opens → app parses the link → Convex creates:

locations { ownerId: Ravi · label "The Blue Door Cafe" · lat/lng · source: whatsapp }

Step 2 · Ravi sees his list. Home screen calls getMyLocations() — Convex checks ctx.auth = Ravi → returns ONLY rows where ownerId = Ravi. Priya's cafe appears.

Step 3 · The app knows Priya is a friend. Earlier, Ravi synced contacts → a contacts row with Priya's hashed number matched Priya's users.phoneHash → a connections row was created (accepted). That's why Priya shows up in Ravi's "Send to" list.

Step 4 · Ravi sends the location. He taps "Send to Priya" → Convex creates:

shares { locationId: Priya's-Cafe · fromUserId: Ravi · toUserId: Priya }

Step 5 · Priya gets pinged. Convex looks up Priya's devices → sends Expo Push → her phone buzzes: "Ravi sent you a location".

Step 6 · Priya opens it. getSharedWithMe() returns the share → she sees "The Blue Door Cafe" as a copy in her list. seenAt is stamped. Ravi's original stays his — she can't edit it, only view.

Step 7 · She opens it in Rapido. "Open With" → clean geo:lat,lng URI → Rapido sets the destination. Ride booked. 🛵

Security at every step: every function asks "who's calling?" — Ravi's queries can't see Priya's locations, and Priya can't see Ravi's other places. The share is the only bridge, and it's explicit.

7

Sharing UX — UPI-style

Like Google Pay: share with anyone by number — no friendship needed. And like Uber's trip link: people without the app still get the location, in one tap.

Share by number — the GPay model V2

Search by name → shows your contacts who have PinBook.
Search by number → if the number is saved in your contacts, shows the person's name; if it's a PinBook account, shows a badge; if neither — shows the raw number.

No friendship required. One-way share, like sending money on UPI. Perfect for the auto-driver case: type his number → location goes straight to him.

What if they don't have PinBook? — the Uber pattern V2

Type numberWhatsApp / SMSLinkWeb pageGoogle MapsorBook a ride

The link opens a simple web page with the location name, address and map. One tap → Google Maps. Or book a ride from there. No app, no account.

Delivery: WhatsApp (free) by default; SMS as backup — decision later. The web page is a tiny T3-stack app on Vercel, sharing the same Convex backend.

Receiving — the inbox V2

Shares from anyone — friend or stranger — land in a Received inbox: "Ravi shared The Blue Door Cafe with you". Actions: Open · Save to my places · Block sender. No connection needed to receive — consistent with the UPI model. If spam ever appears later: a "contacts only" toggle, one setting.

Privacy & safety

· One-way & single-share — the driver sees only that one location, never your list.
· Expiring links — the page dies after X hours, or when you revoke it.
· Non-spammy discovery — a small "Get PinBook" footer on the page; the sender chose to share, so it's permission-based.
· Rate limits + block — no blasting numbers, recipients can block.

New collection — shareLinks V2

FieldTypeWhy
tokenstring 🔒unique, unguessable — the secret inside the link
locationembeddedlabel + address + lat/lng snapshot
createdByid→ users · who made the link
recipientPhonestring?"sent to" tracking
expiresAtdate?temporary shares stop working
revokedAtdate?sender kills the link anytime
openedAt / viewsdate / numberdid they open it? tap tracking

Simple example — Ravi & the auto driver 🛵

Ravi is at his cousin Meera's flat and books an auto. The driver asks, "Where to, sir?"

1. Ravi opens PinBook → taps Share by number → types the driver's number → taps Send.

2. The driver's WhatsApp pings with a link.

3. He opens it → a simple page shows: "Meera's flat — 7, Jubilee Hills, Hyderabad, Hyderabad" with a small map.

4. He taps Open in Google Maps → navigation starts. Done!

No app download. No account. No friendship. And in one hour, the link stops working on its own — Ravi's other places stay private.

8

Screens

Home — search + favorites + listAdd — paste link / explainerEdit / NewDetail — map, actions, details
V2: Login (Clerk)RecipientsSend Location
9

Platform reality check

AndroidiOS
Share target (F1 / F2)✓ Libraries⚠ Native Swift ext
Deep links (F3)
Push notifications (V2)✓ via Expo Push⚠ APNs + $99/yr dev account
Testing & release✓ Emulator⚠ TestFlight + review
The honest truth: everything is buildable. The two costs of iOS = the share extension (F1/F2) and App Store publishing ($99/yr + review).
10

Roadmap

P0

Core app — Android first

Expo scaffold → Clerk login → Convex → screens → F1 Android share → F3 Rapido → polish (dark mode, empty states). Est: 2–3 weeks.

P1

iOS launch

Swift share extension + App Group → TestFlight → App Store. Est: 2–4 weeks.

P2

Friends & reach

Contacts matching (hashed) → F5 in-app send + Expo Push → shareLinks for non-users (web page). Est: 3–5 weeks.

P3

Maps & scale

Real map previews, share-to-anyone via link, web companion (T3 stack candidate). Ongoing.

11

Risks, metrics & defaults

Success metrics — reviewed after every phase

· Locations saved per user per week — is it useful?
· Share completion rate — sent → opened.
· Link open rate — shareLinks.
· Weekly active users — close-circle growth.

Review gate: if a phase's feature isn't used, cut it — don't polish it.

Risk register

RiskMitigation
iOS share extension (P1)Thin Swift ext; early TestFlight; paste-link fallback exists
Rapido geo-URI parsing changesShare-sheet fallback always available
WhatsApp / Maps link parsing breaksPaste-link manual entry (in UI) + parser tests
Phone-OTP needed soonerKnown cost ($20/mo) — decision gate at first outside user
Contacts-matching privacySalted hashes + clear in-app explanation
Expo / Convex breaking changesPin SDK versions

Decision defaults — proceed unless you object

Android-first · Google Maps · name "PinBook" · phone OTP later. Any can change — but the plan proceeds on defaults.

12

Still open — your call

1 · iOS timing: Android-only first, or both at once? (iOS costs $99/yr + share-extension work)

2 · Map provider: Google Maps (needs API key) vs Apple Maps on iOS.

3 · App name: PinBook? JagahBook? PinBook 2?

4 · Phone OTP now or later? If day-one must → Clerk Pro ($20/mo) or revisit Appwrite free.