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

Save→Organize→Open→Share

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 location→Share to PinBook→Named & saved→Found 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 place→Open With→Rapido→Destination 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 Send→Priya's phone pings→She views the place→Saves 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 number→WhatsApp link→Web page with map→Open 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 share→Share sheet→PinBook→Parse geo / URL→Save + 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 app→Convex→Expo Push→Friend'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 number→WhatsApp / SMS→Link→Web page→Google 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 + list→Add — paste link / explainer→Edit / New→Detail — map, actions, details
V2: Login (Clerk)→Recipients→Send 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.