Work / Gleam

Gleam

Cross-platform AI recording and sharing app

Mobile app · spiritual journaling

Visit live site

Cross-platform AI recording and sharing app for iOS, Android, and web — offline-first audio capture with searchable transcripts and AI-assisted reflection.

Product

Gleam helps people capture, transcribe, and revisit audio recordings in one place. Recordings stay searchable and organized so insights surface again at the right time, with AI-assisted reflection and daily recaps that connect back to a personal history instead of starting from zero each day.

Tech stack

Monorepo & build: Turborepo + Yarn 4 workspaces; 4 apps (expo, next, storybook, storybook-rn) and 5 shared packages
Clients: Expo SDK 54 / RN 0.81 / React 19 on native, Next.js 15 on web, Tamagui for universal UI, Solito for cross-platform navigation
Backend: tRPC + Supabase (Auth, Postgres, Storage, RLS); Pipedream for async audio post-processing
Offline / sync: PowerSync + SQLite, @powersync/attachments, TUS resumable uploads to Supabase Storage
Audio: expo-av for recording (migration path to expo-audio), react-native-track-player for background-capable playback
Growth & monetization: RevenueCat for subscriptions, Branch.io for deferred deep links and Meta Ads attribution, RevenueCat → Meta CAPI for server-side conversions
Observability: PostHog (product analytics + feature flags) and Sentry (native + API) with a shared context taxonomy (platform, flow, stage, error_category, retry_count, network_state)
Auth: Supabase PKCE + Apple / Google Sign-In; middleware-protected web routes and hook-gated native screens
Infra: Vercel (web), EAS build + OTA updates on dev/staging channels (native)

Unique challenges solved

Shareable content without a download wall

Built a sharing + claim loop that lets anyone open a shared recording in a browser without the app, then converts the share into a signup. Shared gems use a sharing_token (bypassing RLS for reads), and a tRPC claim route ports the gem into the new user's account on signup. The full funnel (share_modal_opened → shared_gem_viewed → gem_claim_succeeded) is measured in PostHog, turning every share into an attributable growth loop.

Offline-first audio capture that doesn't lose recordings

Recordings persist in PowerSync SQLite and upload through a TUS resumable queue to Supabase Storage. They survive force-quit, app restart, and network loss, then reconcile on reconnect. Retries are non-destructive with a 30s → 2m → 10m → hourly ladder capped at 10 attempts. Per-file and global retry actions are both exposed in UI with copy that matches behavior.

Dual-ID reconciliation (client UUID ↔ server bigint)

Every gem has both a client-generated UUID (client_id, stable idempotency key) and a Postgres bigint primary key. PowerSync upserts outbound with .eq('client_id', op.id) so offline-created gems don't collide on sync. Web resolves /gem/:id by bigint; native can resolve by either. This is what lets offline creation and stable shareable URLs coexist.

Silent production bug in the upload pipeline

A PowerSync watch query used NEW.audio_url / OLD.audio_url — valid only inside SQLite triggers, not plain selects — so the watch silently returned nothing and uploads never started for affected devices. Diagnosed from the "gem exists in DB but no file in storage" symptom, fixed with SELECT audio_url FROM gems WHERE audio_url IS NOT NULL, and wrote a support script so CX could triage existing affected users.

Eventual consistency between DB trigger and storage object

The Supabase INSERT trigger fires before the audio file finishes uploading to Storage, so the downstream Pipedream workflow can arrive before the object exists. Downstream treats "object missing" as retryable (backoff) rather than terminal, and uses client_id as the idempotency key so retries and replays don't double-process.

Deep-link + Meta Ads attribution for new installs

Meta ad → Branch link (with $deeplink_path) → install → Branch SDK tracks attribution on first launch, even for unauthenticated users. Expo Router's +native-intent.tsx intercepts Branch tracking IDs before the router tries to match them as routes (prevents Unmatched Route errors). Purchase events flow server-side via RevenueCat → Branch → Meta and RevenueCat → Meta CAPI — no client-side pixel, no duplicate events.

Consent- and ATT-compliant tracking

iOS ATT prompt gates Meta CAPI device-identifier collection. A marketing-consent atom gates all tracking SDKs. Shared telemetry helpers scrub PII by default (no emails, tokens, cookies, or session payloads) so analytics don't leak sensitive fields.

Observability as a product surface

A shared event taxonomy (audio_upload_failed, auth_session_restore_failed, gem_claim_failed, etc.) plus Sentry tag routing (flow=audio → mobile reliability owner, flow=share → growth, flow=api → backend) means product, growth, and reliability each get their own alerts from the same event bus.

Risk-managed SDK migration planning

Two-track plan for expo-av → expo-audio (deprecated in SDK 54, removed in SDK 55): Track A hardens the current stack, Track B swaps the dependency behind parity tests covering start/pause/resume/stop, duration/metering, queue persistence across app restarts, and the downstream readiness contract. Explicit data-continuity constraint: migration must not silently drop local-only queued attachments from existing users.

Interview soundbites