This project is a pnpm workspace monorepo using TypeScript, focused on the Glimpse mobile app. Glimpse is a privacy-first "missed-connections" Expo app designed to help users connect with people they've encountered in real life, with a strong emphasis on user privacy and data security. The app provides a platform for users to "drop" a Glimpse at a specific location, which becomes visible to others who were in the same approximate area after a 4-hour delay. Key features include location-based matching, mutual opt-in for chat, and robust moderation tools.
- The user wants iterative development.
- The user prefers to be asked before making major changes.
- Framework: Expo / React Native with Expo Router.
- State Management: React Context (AuthContext, GlimpseContext) combined with AsyncStorage for persistent data.
- Image Processing:
expo-image-manipulator(v14) for in-app photo cropping. - Styling: React Native StyleSheet, utilizing semantic color tokens (
constants/colors.ts). - Design System: Warm minimal off-white theme (
#F7F6F3background), near-black text (#1A1A1A), monochrome primary buttons with softened rectangle shape (14px radius), subtle borders (#E5E3DE). No dark gradients. Employs an editorial typographic identity. - Logo: A map pin with a heart cutout (SVG,
components/GlimpseLogo.tsx). - Fonts: Inter (400/500/600/700) via
@expo-google-fonts/inter, and Playfair Display (400 Italic) via@expo-google-fonts/playfair-displayfor branding. - Button Radii: Primary action buttons use
borderRadius: 14; smaller action buttons useborderRadius: 12. No full pill shapes are used. - Keyboard Handling:
- A global
<KeyboardToolbar />fromreact-native-keyboard-controllerensures consistent keyboard dismissal. - Major submit handlers call
Keyboard.dismiss()before async operations to prevent UI obstruction. ScrollView/FlatListcomponents containing inputs usekeyboardShouldPersistTaps="handled".
- A global
- Location System:
- Place Selection: Uses a server-proxied Google Places API (New) via the
PlacePickercomponent. A per-search-session UUID token manages billing for autocomplete and details requests. - "Use current location": Leverages Expo geolocation and OSM/Nominatim for reverse geocoding, which is also a fallback if Google Places API is unavailable.
- Privacy Safeguards: Coordinates are fuzzed (~30m), sensitive locations auto-fallback to broader labels, time is displayed as fuzzy labels, matching uses
spatialBucketIdfrom raw coordinates, and Nominatim is rate-limited.
- Place Selection: Uses a server-proxied Google Places API (New) via the
- Core Product Rules: No real-time location sharing, Glimpse pins become visible after a 4-hour delay, matching requires overlapping coarse location (0.01° ~1km grid) and hourly time buckets, location data is purged after 72 hours, mutual opt-in for chat, and block/report controls are available.
- Location Posting Validation: The
@workspace/location-validationlibrary (lib/location-validation) provides a single source of truth for presence-gate constants.- Server: Authoritatively enforces presence gates via Drizzle SQL predicates.
- Mobile Pre-flight: Provides UX hints in
DropPanelbased oncheckPresencePreflight()andrecent-areasresponse, showing soft amber hints for obvious misses.
- Block / Report Moderation: Server-backed (blocks and reports tables). Two-way enforcement filters blocked users from various UI lists and prevents interactions. Client-side invalidation ensures immediate UI updates.
- Descriptor System: A 3-layer progressive disclosure system defined by
lib/descriptorConfig.ts.- Layer 1 (Required):
topColor,heightRange,hairColor. - Layer 2 (Optional):
clothingType,activityTags,accessoryTags,companionContext. - Layer 3 (Refinements): Context-aware refinements based on Layer 2 selections, e.g.,
clothingStyleTags,activityRefinements. Max 180 characters, no handles or phone numbers.
- Layer 1 (Required):
- Profile System:
- Reusable
ProfileCardcomponent displaysUserProfileSnapshotdata:photoUris,displayName,age,height,job,city,bio,religion,politicalViews. - Photo Management: Server-backed via Replit object storage. Uses presigned URLs for direct client uploads, with CRUD operations (
GET /api/me/photos,DELETE /api/me/photos/:id,PUT /api/me/photos) and a cap of 6 photos per user.
- Reusable
- Screens:
/auth(welcome, sign in, sign up)/location-onboarding(privacy, permissions)/(tabs)/index(My Glimpses)/(tabs)/discover(Discovery)/(tabs)/drop(Drop a Glimpse with 3-section descriptor UI)/(tabs)/responses(Incoming responses)/(tabs)/profile(Profile tab with Edit/Preview, photo management)/glimpse-detail(Full descriptor view, respond/block/report)/chat(Private chat, mutual unlock only)/profile-edit(Redirects to profile tab)/settings(Subscription, blocked users, privacy info)
- Account Deletion: Implemented via
DELETE /api/me, which deletes the Clerk user and cascades deletions across all related database tables. The process handles Clerk 404s as success for idempotency and returns 500 on DB failure to ensure retry. Client-side,deleteMyAccount()is called beforesignOut(). - UI Tweaks:
- Onboarding transitions:
screenOptions.contentStyle.backgroundColorset tocolors.backgroundto prevent black flashes. - Profile-setup photo grid: Refactored photo tiles with gradient scrims, re-positioned
removeBtnandmainBadge, consolidated reorder chevrons into areorderPill. Empty required slots now have varied affordances. GlimpseCardlayout:descriptors.headeris a bold title,descriptors.noteappears in a quote block only if present.- Glimpse deletion: Author-only delete (
DELETE /api/glimpses/:glimpseId) with schema cascading for related data. UI includes a destructive button and confirmation modal.
- Onboarding transitions:
- API Framework: Express 5.
- Build: esbuild (CJS bundle).
- Global Error Handler: Returns structured JSON
{error,code:"internal"}for 500 errors. - Rate Limiters: Implemented on
POST /storage/uploads/request-url,PATCH /me,POST /me/photos,PUT /me/photos,POST/DELETE /me/blocks/:userId. - Moderation:
POST /reportsrunsisInappropriateon thedetailfield before persistence.
- Database: PostgreSQL (Drizzle ORM for schema management and queries).
- Authentication: Clerk (via
@clerk/expo). - API Codegen: Orval (generates code from OpenAPI spec).
- Validation: Zod (
zod/v4) anddrizzle-zod. - Location Services:
- Google Places API (New) for venue search (server-proxied).
- OpenStreetMap (OSM) / Nominatim for reverse geocoding (for "use current location" and as a fallback).
- Expo Geolocation (for current location detection).
- Image Handling: Replit object storage (for user-uploaded photos).
- Analytics: Custom event logging system integrated with client and server.
Status of pre-submission blockers (last audit: see chat history):
Resolved in code
- Privacy/Terms links (Apple Guideline 5.1.1):
artifacts/glimpse/lib/legalLinks.tsexposesTERMS_URLandPRIVACY_URL, fed byEXPO_PUBLIC_TERMS_URL/EXPO_PUBLIC_PRIVACY_URLenv vars (placeholder defaults togetglimpse.app/{terms,privacy}). Surfaced in two places:app/auth.tsxwelcome screen ("by creating an account you agree to…") andapp/settings.tsxAbout section. Both callLinking.openURLwith a fallback Alert on failure. - iOS / Android permission strings:
app.jsonios.infoPlistdefinesNSLocationWhenInUseUsageDescription,NSCameraUsageDescription,NSPhotoLibraryUsageDescription.android.permissionslistsACCESS_COARSE_LOCATIONandACCESS_FINE_LOCATION. - Schema sync on deploy:
artifacts/api-server/.replit-artifact/artifact.tomlproductionbuild.argsis nowsh -c "pnpm --filter @workspace/db run push && pnpm --filter @workspace/api-server run build", sodrizzle-kit pushruns against the deployment'sDATABASE_URLbefore the server bundle compiles. Build fails loudly if push errors (intentional — better to block deploy than ship a stale-schema server).
Manual / out-of-code (must do before TestFlight upload)
- Set
EXPO_PUBLIC_DOMAINin EAS production build profile to the live deployment domain (NOT the dev Replit domain). - Provision Clerk production instance; set
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_…in EAS,CLERK_SECRET_KEY=sk_live_…in deployment secrets. - Set
EXPO_PUBLIC_TERMS_URLandEXPO_PUBLIC_PRIVACY_URLin EAS to live, publicly-reachable URLs. Same URLs go in App Store Connect → App Information → Privacy Policy URL. - Provision a separate production PostgreSQL (do NOT reuse the dev
DATABASE_URL); set the prod URL only in deployment secrets. - App Store Connect privacy questionnaire: declare email, display name, age, photos, precise location, user-generated text. All under "App Functionality" / "Authentication".
- Demo reviewer account (
appreview@…) created in Clerk production, run through onboarding once so a profile exists; paste credentials in App Review notes. - App Review notes should explain location use (presence-gate, anti-spam, coarse coordinates, never shared with other users).
Design decisions
- 18+ gate is enforced at profile-setup (
app/profile-setup.tsx:107-115), not signup. Acceptable because onboarding is mandatory before any in-app action. - Schema sync uses
drizzle-kit pushrather than versioned migrations to keep parity withscripts/post-merge.sh. If/when the project moves to a true migration workflow (drizzle-kit generate+migrate), update bothpost-merge.shand the production build args together.