A real, beautiful, brand-aligned waitlist page at /hallpass with a 9-field application form, plus the admin triage queue that turns approved applications into actual Hall Pass grants in one click. The program now has a public mouth, not just a backstage door.
- Migration 00042 creates hall_pass_applications with full applicant payload — name, email, primary platform + per-platform handles in JSONB, audience size band, niche (mapped to the 16 diversity verticals from CLAUDE.md), requested tier + months, two long-form fields (why DOLLA + how-they-bridge-their-audience), referral source, status enum (pending → contacted → approved/rejected → converted), reviewer email + notes + reviewed_at, granted_pass_id link to the resulting Hall Pass row, and IP + user-agent for triage signals
- Partial unique index on (lower(email)) where status='pending' enforces 'one pending application per email' at the database level — translates to a friendly client-side message instead of a duplicate-row insert error, and reopens automatically once the prior application is resolved
- RLS locked all the way down — no client policies — so reads happen exclusively through the service role from /api/admin/hall-pass-applications. The public submit endpoint is the only write path that hits this table from outside the admin
- Public page at /hallpass: brand-DNA hero with purple-themed orbs, four 'what you get' perk cards across complementary tier / growth coaching / ad-creative guidance / featured discovery, an explicit 'what we ask in return' covenant block that lifts the four disciplines from the Creator Playbook, and the form itself in a purple-gradient card
- HallPassApplyForm: client island with section-grouped fields, per-platform handles as a 5-input cluster, native HTML5 validation backed by server-side validation, an explicit values-aligned agreement checkbox (gates submit), and a celebratory inline success state on 201 — no full-page redirect since the whole flow is one page
- POST /api/hall-pass/apply: public, no auth. Captures submitted_ip + user_agent for triage signals, auto-links to a DOLLA account if one already exists for the email, and surfaces the database's duplicate-pending block as a human message instead of a 500
- Admin queue at /admin-dashboard/hall-pass-applications: status-tabbed list (pending / contacted / approved / converted / rejected / all), per-application card showing identity + platform + audience + niche + ask, full-application <details> drawer for the long-form fields + review notes + linked granted-pass id, and an inline ApplicationActions client island with notes textarea + four buttons (mark contacted, approve, reject, convert)
- Convert flow: PATCH for status changes, POST to convert. Convert calls grantHallPass under the hood — same code path as the per-user admin card — so the resulting pass is identical in every way to a directly-granted one. The application row's granted_pass_id then deep-links to the user's drill-in page so the audit trail closes the loop
- Convert is gated client-side AND server-side: if the applicant has no linked DOLLA account, the button is disabled with a tooltip explaining 'ask them to sign up first' (since there's no users.id to bump). user_id is re-resolved at conversion time in case they signed up between submission and approval
- Admin layout sidenav now carries 'Hall Pass apps' with a checkmark glyph alongside Marketing, so the queue is one click away from any admin surface — and the URLs sitemap lists all six new routes so a new collaborator finds them immediately
A formal, time-bounded, fully-audited comp tier program built into the admin surface. We gift selected affiliate creators top-tier access for a fixed window (typically 3 months) while we coach them on bridging their existing audience into a DOLLA community — social-media playbook, ad guidance, post cadence — and the platform handles the lifecycle automatically. The user sees a dignified 'thank-you' banner on their tier-settings page; the admin sees a full grant + revoke + history surface; the daily cron handles the gracious revert to free at expiry without a single human touch.
- Migration 00041 creates the hall_passes table with tier (verified | sovereign), duration_months (1–24), required reason, granted_at + expires_at, granted_by_email, optional revoked_at + revoked_reason, an expiry_processed flag for idempotent cron processing, and prior_premium_tier + prior_creator_tier snapshots so expiry restores exactly the tier the user had BEFORE the gift instead of collapsing every comp to free
- RLS policy hall_passes_user_select_own gates reads to the row's own user — the banner on /settings/tiers can render via the browser Supabase client without exposing other users' grants, and writes go through the service role from admin routes only
- Layered on top of the existing admin_tier_override mechanism: granting a pass bumps users.premium_tier and/or creator_tier to the gifted tier, flips admin_tier_override = true with a 'Hall Pass · {reason}' note so verify-tier's self-heal won't claw it back, and snapshots the prior values onto the pass row for clean restoration on expiry or revocation
- Admin grant + revoke API at /api/admin/users/[id]/hall-pass (POST grant + GET history) and /api/admin/users/[id]/hall-pass/[passId] (DELETE revoke). Grant blocks if an active pass already exists (the partial unique index hall_passes_one_active_per_user_idx enforces this at the database level too — defence in depth)
- Admin UI: a new Hall Pass card on /admin-dashboard/users/[id] — purple-accented to distinguish it from the cyan tier-override card, shows the active pass with days-remaining + reason + revoke button, exposes a grant form with tier selector, duration dropdown (1 / 2 / 3 / 6 / 12 months), checkboxes for premium-side vs creator-side application (default both), and a free-text reason input that defaults to 'Affiliate creator — onboarding & growth coaching' but is fully editable
- User UI: a HallPassBanner component renders at the top of /settings/tiers when an active pass exists for the signed-in user. Format reads '[Tier] [N]-Month Hall Pass · Expiring in [days] days' with a ticket-icon glyph in a tier-tinted card (Verified = green, Sovereign = purple), warm one-liner about the gift, and a small monospace expiry date
- Daily expiry cron at /api/cron/expire-hall-passes (scheduled 04:30 UTC, 30 minutes after the stories cron so the two never collide) walks every pass past expires_at, restores the user's snapshotted prior_premium_tier and prior_creator_tier, clears admin_tier_override + its by/at/note fields, marks expiry_processed = true so reruns are idempotent, and returns a per-pass error array so any one failure can't block the rest of the sweep
- Failure handling: revoking and expiring share the same restoration path — both put the user back under payment-governed verify-tier so their tier ends up wherever their actual paying state warrants (free if no sub, verified if they were subscribing on the side, etc.) without any hand-tuning
- Pass history is preserved indefinitely on the row even after expiry or revocation — the admin card surfaces the last 20 grants per user with status (active / revoked / expired) so the affiliate program's track record is auditable per creator
- Same-day hardening on the admin user drill-in: HallPassCard's type imports were inlined locally so a 'use client' component can never accidentally pull lib/hall-pass.ts (which references getSupabaseAdmin) into the browser bundle, and the page-level Promise.all gained .catch() guards so a brand-new feature can never break the entire admin user-detail render
- Surfaced and fixed an unrelated latent bug while debugging a 404 on the user-detail page: the loadUser SELECT referenced four follow_limit_override* columns whose migration (00024) had silently never been applied to prod — applied it, plus patched loadUser to console.error any future SELECT failures so the next variant of this bug shows up in Vercel logs instead of as a silent notFound()
Hours-long forensic on why iMessage and other share crawlers were rendering DOLLA links with the bare $ favicon glyph instead of the full hero card. Two interlocking bugs (one in metadata, one in the icon set) had both regressed silently; both fixed and verified live in production HTML.
- Root cause #1 — Next.js metadata merge replaces, not merges. The site's /home, /coinbase-application, and many other SEO-oriented routes set their own openGraph block to override the layout's per-page title and url; per Next.js's merge rules, that REPLACES the parent's openGraph entirely and silently drops the file-convention auto-injection from app/opengraph-image.tsx. Result: production HTML shipped with no og:image and no twitter:image meta tags at all, so iMessage/Facebook/LinkedIn/Slack all fell back to the favicon
- Fix: explicitly list /opengraph-image (and the twitter equivalent) in openGraph.images and twitter.images on the three highest-traffic routes — root layout, /home, /coinbase-application — so metadataBase resolves the relative path to https://justadolla.com/opengraph-image for every share crawler. Verified live: og:image, og:image:width, og:image:height, og:image:alt, twitter:image all now present in production HTML on both routes
- Root cause #2 — favicon.ico was 4 days older than the rest of the icon set. apps/web/app/favicon.ico last changed 2026-04-25 while icon.svg + apple-icon.tsx + manifest.ts all moved to the current cyan-$ design on 2026-04-29. Mobile Chrome's URL-bar favicon comes from favicon.ico, so the address bar showed the old icon while bookmark tiles and the predictive dropdown (which read from icon.svg / apple-icon / manifest icons) showed the new one
- Fix: scripts/regenerate-favicon.mjs renders the canonical icon.svg at 16/32/48 px via sharp and packs them into a multi-size ICO with PNG-encoded payloads (modern browsers + mobile Chrome accept this format). Idempotent — re-run any time icon.svg changes via `node scripts/regenerate-favicon.mjs`. Added sharp as a workspace devDependency since pnpm doesn't hoist it from its transitive position under Next.js
- 21 other routes (calculators, blog, /breakdown, /minted, /alternatives, /for, /cities, /causes, /topics, /about, /creator-playbook, /business, /insights, /wallet-funding, the campaign route) still set openGraph without images and inherit the same trap — queued for a follow-up sweep behind a small lib/seo helper that returns the standard image objects so each page edit is a one-liner
Shipped a dedicated, brand-aligned reassessment brief for the Coinbase Onramp partner team in support of DOLLA's path to standard production access. One link, ten sections, every architectural and compliance argument we'd otherwise be sending as attachments — all rendered with the rest of the site's design language so the case lands the same way the product does.
- Single-page brief at /coinbase-application: executive summary + ask, product overview, the precise Onramp user-journey we run (six steps from intent to settlement), nine-point policy compliance walkthrough, the non-custodial architecture explained at the contract level, content policy + trust & safety enforcement, corporate structure under Kingdom Portfolios LLC and Kingdom Seed Foundation, eight live risk controls, three concrete asks for the meeting, and an appendix with every Base mainnet contract address we touch
- Sticky desktop table of contents with 10 anchors so a reviewer can navigate the document the same way they'd navigate a spec — and so the founder can drive the conversation live without scrolling
- Three asks framed for the meeting: (1) reopen the application with stated reasoning if a re-decline happens, (2) written confirmation of the approved use-case scope at production, (3) a named technical contact across Onramp / OnchainKit / CDP so future ecosystem questions land in the right inbox
- noindex via metadata — link-shareable for the partner concierge but not surfaced in search; OG card metadata set so the link unfurls cleanly in iMessage, Slack, and email previews
- Source-of-truth markup also lives at docs/coinbase-onramp-reassessment.html as a printable single-file leave-behind with a print stylesheet that drops the dark theme for clean PDF export
- Brand consistency: same ambient orbs, font-mono uppercase tracking-widest pills, font-serif italic accent, and dark glass aesthetic as /breakdown — the case for production reads as the same product the case is about
Overnight rebuild of the story surfaces from a working but rough prototype into something that reads, gestures, and renders the way Instagram and TikTok have trained users to expect. Every piece — tray, creator route, viewer, gestures, audio, accessibility — touched and improved against direct on-device feedback from the founder testing each ship in real time.
- Migrations 00035-00038 applied to prod via the patched db-push script (the script's regex broke when the Supabase Custom Domain swap put 'auth' where the project ref used to be — now resolves the ref from the pooler URL's username so it survives any client-domain rewrite). Stories table, live_streams table, 80 seed stories across diversity verticals + a demo replay, and the public Storage bucket are all live
- Camera promoted from a fixed-overlay modal to a real route at /story/new with a slide-in-from-left animation — iOS Safari's edge-back swipe and the browser back button both dismiss it the same way they dismiss any other page; the URL is shareable and refreshable
- Live-camera capture parked after iOS WebKit autoplay rules made the inline viewfinder unreliable across devices (black-screen-after-permission-grant on iOS Chrome despite multi-event play retry, defensive track.enabled, and a tap-to-start fallback). Replaced with the iOS native photo/video picker — same path Instagram uses on mobile web — which works identically on every iOS browser. Camera component preserved in-tree for the eventual native-shell or service-worker workaround
- Viewer fully ported to a document.body portal so the tray's backdrop-blur ancestor (which per CSS spec creates a containing block for fixed-positioned descendants) can never trap the player at sub-viewport scope. Z-index maxed at 2147483647, body scroll locked while open, entry animation a 200ms fade + scale-up
- Gesture handler unified onto pointer events with proper hold-vs-tap-vs-swipe disambiguation: 200ms decides hold-to-pause vs tap-to-advance, axis-locked on first move >10px decides horizontal swipe (jumps to next/prev creator group, IG-style) vs vertical swipe (close at >80px). Tap left third = previous, right two-thirds = next
- Audio: video stories start muted (autoplay-friendly across iOS WebKit and Chromium), with a top-right mute toggle button. Preference persists via sessionStorage so the viewer doesn't have to re-unmute every story. TikTok-style 'Tap to unmute' coachmark fades in for 3s on the first muted clip in each session
- Visual feedback: 200ms dim + chrome opacity drop while held (pause is now legible without hiding the media), centered spinner during video buffering driven by waiting/canplay events, captions render multi-line with text-shadow for legibility on bright media
- Creator strip on the player: avatar + name wrapped in next/link to /[username] when the API supplies a username — tap jumps to the creator profile and auto-closes the player. The /api/stories endpoint now returns username + displayName + avatarUrl per group inline, eliminating the 80-parallel-fetch N+1 pattern the tray was doing
- Triple-dot menu: top-right ellipsis opens a small popover with Copy link (writes /story/<id> to clipboard) and Report story (POSTs the new /api/reports route which lands rows in the existing reports table for admin triage; rate-limited like comments, dedupes on (reporter, target, status='pending'))
- Reply input bar at the bottom — IG's 'Reply to <name>…' pattern, focusing the input pauses the current story so the user isn't fighting a 5s timer. Three quick-reaction emojis sit alongside as one-tap shortcuts. Send currently fires a heart reaction + toast pending a real DM backend; UI is in place so the moment we wire DMs the path is one route call away
- Per-viewer seen state persisted server-side via the story_views table (was previously local-session only), surfaced through /api/stories so a watched ring stays grey across visits / devices. Tray sort order matches IG: caller's own first → unwatched groups → watched groups
- Pause-on-visibility-change so a backgrounded tab doesn't burn through stories the user can't see. Pre-loads the next story's image AND the next group's first image so cuts feel instant on cellular. Safe-area insets respected on top progress bars + bottom reply input + reaction bar
- /api/cron/expire-stories tightened to a once-daily 4am UTC schedule (Vercel Hobby plan limit) — story media stays alive 24h + 24h grace before hard-delete, well inside the cron window so the worst-case stale-row never exceeds the grace period
Ephemeral 24-hour stories with a 3-minute video cap and full IG/Snap interaction model, plus creator broadcasts with HLS playback, real-time chat, and concurrent viewer presence. Both surfaces use the same tier-gated architecture as posts (free / $1-mo / $1-wk), populate isLive across every feed query, and ship behind a provider-agnostic streaming adapter so Cloudflare Stream Live can plug in tomorrow without code changes.
- Stories backend (migration 00035): stories + story_views + story_reactions tables with RLS, partial indexes for active-by-creator, and CASCADE cleanup. 3-min video cap enforced at API + DB. /api/stories POST creates with 24h TTL, GET returns groups visible to the caller (free + creators they follow at the right tier), [id]/view UPSERTs first-frame views, [id]/react toggles emoji from the fixed vocabulary (heart / fire / pray / clap / wow / laugh)
- Stories UI: StoryRing (conic-gradient ring that greys when seen) + StoryPlayer (full-screen, tap-zone advance, hold-pause, swipe-down close, keyboard arrows, reaction bar) + StoryCreator (probes video duration client-side, uploads to Supabase Storage, then POSTs) + StoriesTray (caller-first +bubble, then active groups). Wired into /following and /discover above the feed
- Stories cleanup: /api/cron/expire-stories runs every 6 hours, hard-deletes stories past a 24h grace window plus their underlying Supabase Storage objects. CASCADE handles views + reactions. Wired into vercel.json crons with bearer auth
- Live streaming backend (migration 00036): live_streams + stream_chat_messages + stream_viewers tables. Provider-agnostic — DOLLA owns the state (status, peak, replay), the provider only pipes video. Currently-live partial index keeps the LIVE indicator query cheap. /api/streams POST provisions a stream + persists ingest secrets, GET lists live streams visible to caller, [id] PATCH ends the stream and pulls the replay URL, [id]/chat does Realtime delivery + durable persistence, [id]/viewers heartbeats for last-seen totals
- Streaming provider adapter: lib/streaming/provider.ts exposes a 3-method interface (createStream, endStream, getReplay) with two implementations — cloudflare.ts for Cloudflare Stream Live ($1/1k min delivered, recordings included) and mock.ts that returns Apple's HLS test stream so dev/preview never depends on cloud creds. Falls back to mock automatically when CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_STREAM_API_TOKEN are missing — production wires real keys in Vercel envs and the adapter takes over with zero code change
- Live streaming UI: GoLiveButton replaces the dead 'Live' indicator in the creator dashboard header — opens a preflight modal that respects creator_tier (monthly/weekly options locked for free creators), provisions the stream, and routes to the broadcaster page. BroadcasterPanel shows RTMP URL + stream key with copy-to-clipboard and OBS instructions. /dashboard/live/[id] is the broadcaster view (panel + chat side-by-side); /live/stream/[id] is the public viewer with HLS playback; /live/[username] is the share-friendly URL that resolves to the active stream or falls back to the most recent replay
- HLS playback: Safari/iOS plays natively via <video>; everywhere else dynamic-imports hls.js — Safari users don't pay the bundle cost. Live latency tuned with liveSyncDurationCount=3, maxBufferLength=30. LiveChat hydrates last 100 messages then subscribes to a Supabase Realtime broadcast channel keyed by stream_id (instant delivery) AND posts to the durable API (so latecomers can replay the conversation). LiveViewerPresence uses presence channels for free real-time concurrent count plus 30s heartbeats to /viewers for total-watch-time totals
- isLive wiring: getLiveCreatorIds() helper in lib/queries.ts joins live_streams.status='live' once per request and populates isLive across all 5 callsites — getCreators, search, getCreatorByUsername, the feed assembler, and trending. Following page now links live creators to /live/[username] instead of profile so a click on a red-ringed avatar opens the stream
- Demo seed (migration 00037): one captioned free-tier story per seed creator with content-aware Unsplash images by niche (fitness, garden, faith, art, cooking, code, music, charity, environment), plus an ended stream with a replay URL on the top seed creator. Demo mode and screenshot reels look populated from minute one
- Smoke coverage: e2e/stories-and-live.spec.ts confirms the public surfaces don't 500, /api/stories + /api/streams shapes are correct, unauthenticated POSTs return 401, /live routes survive unknown ids without crashing. Typecheck clean across the entire web app
- Tomorrow's wire-up: apply migrations 00035-00037, create the public 'stories' Supabase Storage bucket, add Cloudflare Stream Live keys to Vercel envs (STREAMING_PROVIDER=cloudflare + CLOUDFLARE_ACCOUNT_ID + CLOUDFLARE_STREAM_API_TOKEN), then OBS → RTMP → /live/<username> for end-to-end verification
A public manifesto + practical guide that plants the disciplines, mechanics, and cultural pillars new creators need on day one. Reframes the creator-growth question from 'what algorithm should we build?' to 'what culture do we plant?' — because the disciplines that make creators succeed are upstream of any algorithm.
- Six numbered sections at /creator-playbook: The Five Disciplines (Show up / Show your work / Show up FOR people / Show what you're for / Show the path), The Mechanics That Reward This (all 5 achievement tracks + Founding Creator badge), The Way We Operate (4 cultural pillars), The Quarterly Rhythm (Week 1 / Month 1 / Quarter 1 floor expectations), Sprints + Competitions (Minting Season / Streak Rewards / Charitable Match / Founding Creator Cohort), and What Doesn't Work Here (explicit anti-patterns)
- Each discipline is tied to a specific Achievement Track (Influence, Impact, Retention, Philanthropy, Mint) so creators can see exactly which behavior unlocks which platform-visible reward
- Founding Creator badge publicly named — the first ~25 hand-recruited creators across diversity verticals get a permanent profile mark and a private founders' channel. Creates urgency for the cohort phase without false scarcity
- Cross-platform funnel guidance baked in: 'DOLLA isn't where they discover you; it's where they commit to you.' Every creator's TikTok / IG / YouTube becomes a top-of-funnel for $1/mo follows
- Anti-patterns section publicly states what doesn't work: engagement-bait, reposting, transactional upgrade pressure, hiding the mission, treating $1 as nothing. Builds trust by being honest AND filters culture-mismatched creators before signup
- Funnel attribution: pixels fire on view + section-depth via Clarity tags + CTA clicks. Conversion path /home → /creator-playbook → /signup?creator=true is end-to-end measured
Wallet friction is the #1 conversion killer for crypto-native creator platforms. New page meets visitors wherever they are: brand new to crypto, has Coinbase but USDC isn't on Base, or already fully set up. Each path is honest about time required and routes to the right next step.
- Path A (~15 min): brand new to crypto. Open keys.coinbase.com, create Smart Wallet with passkey (no seed phrase), fund via Coinbase Onramp (Apple Pay / debit / bank), return to DOLLA. No exchange account required
- Path B (~5–10 min): already on Coinbase exchange. Create Smart Wallet, send USDC over Base network (NOT Ethereum — saves $5–$30 per transfer in gas)
- Path C (~30 sec): already on Smart Wallet with USDC on Base. Single CTA to /signup where wallet sign-in lives
- Funnel attribution: pixel + GA4 + TikTok + Pinterest + Clarity events fire on initial view AND on segment selection (with chosen segment as content_id). Tells us which audience type is highest-intent and where to invest more polish
- Linked from /home and /signup at the moments first-time visitors hit wallet-related friction. /home now offers two helper paths under the primary CTAs — 'New to crypto?' (consumer route) and 'Thinking of creating?' (creator route, links to the Playbook)
- SEO-targeted at long-tail crypto-onboarding queries — 'fund Coinbase Smart Wallet on Base', 'USDC on Base Onramp', 'Coinbase Smart Wallet setup'
Migration 00031 applied to prod. Background-job failures (cron charge errors, paymaster issues, route-handler exceptions) now land in a queryable table with stack, surface, and structured context — replacing the previous 'silent log to console + warning' fallback.
- First-party error tracking without taking on Sentry's pricing or SDK weight at this stage. Server handlers call logServerError(error, context) which writes one row per uncaught error
- Captures: occurred_at, message, stack, error_name, surface (route path / 'cron' / 'background'), structured JSONB context, fingerprint (for trend analysis), resolved flag
- Indexed for the queries the admin dashboard will need: occurred_at DESC, fingerprint, surface
- Replaceable: when Sentry / Highlight / Datadog get funded, swap the implementation in lib/error-log.ts and keep this table as a redundant write OR drop it. The logServerError() signature is the durable contract
DOLLA now has the full distribution-measurement stack a serious creator economy needs: every visitor builds remarketing pools on five ad platforms in parallel, every session is replayable for product debugging, and two server-side conversion APIs are ready for when paid acquisition begins.
- Five client-side retargeting pixels live and collecting broad audiences in parallel: Meta Pixel, Google Ads tag, TikTok Pixel, Pinterest Tag, plus Google Analytics 4 for traffic attribution. Each fires on first paint AND on every SPA route change so single-page navigation is counted accurately
- Microsoft Clarity wired for free unlimited session recordings + click heatmaps + scroll-depth + rage-click detection — turns 'why is this page bouncing' from a guess into a watchable replay
- Two server-side Conversions APIs provisioned with access tokens stored in Vercel: TikTok Events API and Pinterest Conversions API. These bypass iOS 14.5+ tracking restrictions and ad blockers when conversion optimization gets switched on later
- Strict no-Tag-Manager policy: every pixel installed inline directly in <head> for fastest first-paint, no GTM round-trip latency, no risk of duplicate firing from container misconfiguration
- Typed wrapper modules (lib/pixel.ts, lib/google.ts, lib/tiktok.ts, lib/pinterest.ts, lib/clarity.ts) make per-platform conversion event wiring a one-line call site change when the time comes
- Broad-retargeting-only strategy by design: no specific Conversion Actions or audiences defined yet. The infrastructure passively builds 'all visitors' pools across five platforms while the product matures — paid campaigns can attach to warm audiences on day one with zero cold-start period
A schema mismatch on the analytics table had been silently throwing on every interaction since 2026-04-15, cancelling the like/save/reshare writes that ran after it. Found via direct prod-DB audit, fixed via migration plus route hardening.
- Diagnosed by querying the prod tables directly — likes / reshares / saves had ZERO new inserts in the prior 14 days while comments kept landing through their separate route, isolating the bug to the shared interactions endpoint
- Root cause: feed_interactions table held the original boolean-rollup schema (did_like, did_reshare, watch_time_seconds) but the route writes the event-stream schema (interaction_type, feed_type, position, duration_seconds). Every insert errored on missing columns and threw, killing the engagement upserts that should have run next
- Migration 00034 drops + recreates feed_interactions with the event-stream schema, plus drops + recreates the dependent post_engagement_stats materialized view (now keyed on duration_seconds for the avg-watch-time calc)
- Route hardened: the feed_interactions insert no longer throws on failure — analytics is best-effort, engagement is critical path. Future schema drift on the analytics table can never silently wipe out user-facing writes again
- Comments unaffected throughout — they go through /api/comments, which never touched feed_interactions
The Google OAuth consent screen, magic-link emails, and storage URLs now read 'auth.justadolla.com' instead of the random Supabase project ref. Brand consistency across every authenticated touchpoint.
- NEXT_PUBLIC_SUPABASE_URL clients (web + mobile) point at auth.justadolla.com — same Supabase project, branded host
- next.config.ts images allowlist updated so storage-served avatars + post media on the new hostname flow through the optimizer
- Mobile globals: 16px font-size on inputs/textareas at ≤768px suppresses iOS Safari's focus auto-zoom; touch-action: manipulation on interactive elements kills the double-tap-zoom drift
- App icon reverted to the master DOLLA$ logo (Apple touch icon untouched)
- RUNBOOK updated to note the Custom Domain auth host alongside the direct DB host so future deploys don't accidentally fall back to the project-ref URL
🥩 Steak-dinner momentSigned-in users now land on /discover (the feed). Signed-out visitors get the marketing pitch at /home. The single root URL finally tells two different stories — the way every real social product works.
- / became a server-rendered splitter that reads Supabase session cookies and redirects in <50ms — no flash, no client-side flicker, no double paint
- Signed-in cohort: every return visit drops them straight into the feed. The platform feels owned, not visited. This is the IG/Twitter/TikTok pattern that retention is built on
- Signed-out cohort: still gets the full marketing pitch, conversion CTAs, OG share image, Pixel events — moved verbatim to /home so nothing was lost
- TopNav logo points to /home for everyone, so signed-in users can always click back to the pitch (useful for sharing the story to friends mid-session)
- Structural shift, not cosmetic: this is the moment the product graduates from 'demo with a homepage' to 'real social app where the URL bar means something'. Every growth metric — DAU/MAU, session length, return rate — gets its honest baseline starting today
Single Smart Account → 10 deterministic shards. Capacity ceiling jumps from ~100k paying subs to ~1M.
- Provisioned 10 CDP Smart Wallets (dolla-subscription-owner-shard-00 through -09) on Base mainnet
- Each new subscriber is hashed (FNV-1a on user_id) → assigned to a deterministic shard. User signs the spend permission against THAT shard's address
- wallet_name persisted on subscriptions + snapshotted on the charge job so the worker calls charge() against the matching wallet later
- Verified the project-wide Coinbase Paymaster auto-sponsors all 10 — no manual allowlist needed
- Existing legacy subs (wallet_name NULL) keep working unchanged via resolveWalletNameForCharge fallback
- Migration 00019: subscriptions.wallet_name + charge-job snapshot. Idempotent provisioning script: provision-subscription-owner-shards.mjs --count=N
Serial walker → fan-out queue. Throughput jumps from ~10 charges/sec to ~250/sec. Cron timing out is no longer existential.
- Migration 00018: subscription_charge_jobs table with UNIQUE(subscription_id, period_end) for idempotent re-enqueue
- claim_charge_jobs(batch_size) RPC using FOR UPDATE SKIP LOCKED — multiple workers each claim a disjoint batch atomically
- /api/cron/charge-subscriptions (daily) ENQUEUES jobs only, then fires 5 parallel HTTP workers
- /api/cron/charge-worker (new) — claims a batch via SKIP LOCKED, processes with Promise.allSettled, exits cleanly
- lib/charge-job.ts: shared processChargeJob handler. Failed charges back off exponentially (1h → 6h → 24h) up to 3 attempts. Past grace deadline → mark sub expired (preserves old behavior)
- Trivially swappable to Inngest / Vercel Queues later — no schema changes needed
One-tap signup that creates the account AND links the wallet. No password, no email.
- Apple removed everywhere — provider was returning 400 on signup and we don't depend on it for any flow
- New /api/auth/wallet-signin/nonce + /api/auth/wallet-signin routes — server-issued nonce, viem.verifyMessage handles ERC-1271 + EIP-6492 (counterfactual Smart Wallets work)
- Server mints a one-time Supabase magiclink token via admin.generateLink — NO email is ever sent
- Client exchanges via supabase.auth.verifyOtp(token_hash, type:"email") for a real session
- Wallet address is auto-linked to the user's row on first sign-in — ready to follow / tier-sub / donate immediately
- EOAs rejected at the door (DOLLA needs Smart Wallet for recurring billing)
Photos, GIFs, and videos all upload from one button. Videos autoplay muted with IG-style scroll pause.
- New post page accepts image / GIF / video via single picker, per-kind size caps (8MB / 16MB / 100MB)
- Feed video card: muted + loop + playsInline + IntersectionObserver (plays at ≥50% visibility, pauses when scrolled away — same window IG/TikTok use)
- Bottom-right speaker toggle for opt-in audio
- Post detail page renders autoplaying <video controls> when first media is video, image otherwise
- GIFs animate natively via the existing <img> path — no extra code needed
iOS app boots, signs in via Supabase, and pulls real posts from justadolla.com.
- lib/supabase + lib/api: Supabase client with AsyncStorage token persistence; fetch wrapper forwards Bearer tokens so the same getAuthUser() server-side resolver works for mobile
- (auth)/login + (auth)/signup wired to Supabase signIn / signUp
- (tabs)/discover, trending, following all fetch from /api/feed/* — replaced mock data adapter
- _layout: AuthProvider + auth-gated routing (signed-out → /login)
- Brand kit v2.0 colors applied (Sovereign Purple, Brand Gold, Verified Green) matching web globals.css
- Wallet integration deferred with a written 3-option decision doc (Privy embedded / Coinbase deep-link / WebView wrapper) — recommendation: Privy
5 progression tracks fire on every payment. Mint moments surface a notification to the creator who brought the new user in.
- lib/achievements: runAchievementCheckBackground() + sendMintNotification() — direct in-process call, no HTTP roundtrip
- Wired into follow/confirm, donate/confirm, donate/record-onetime, cron/charge-subscriptions
- Income / Philanthropy / Influence / Impact / Mint tracks all evaluated against live stats from payments + donations + follows + mints + post engagement
- AchievementsBoard: collapsible IG-style pill on profile ("⭐ N/5 cards · View stats ▼") — expands to the full 5-track grid + lifetime stats
- "Minted by @creator" gold pill on profiles where applicable — the user who brought them in is permanently tagged
709 seeded placeholder accounts hidden from production. Pitch-deck view still one query param away.
- Migration 00015: users.is_seed + posts.is_seed columns
- 709 seed users + 1020 seed posts flagged via UUID prefix, corp-wallet placeholder address, seed_creator_metrics override row, or generated name+digit pattern
- All feed / search / creators queries default to is_seed=false, accept includeSeed flag
- API routes read ?demo=1 via lib/demo-mode; client pages propagate via useDemoQuery hook (window.location.search read inside useEffect — no Suspense bailout)
- Following feed intentionally NOT gated (if you actively follow someone, you want their posts)
- Real users currently visible in production: 3 — kingdomportfoliosllc, affluenceer, estremeraevan
One Coinbase Smart Wallet per user — both follows AND donations route to wallet_address.
- Donor sub-account architecture was attempted (sprout-donor-wallet flow) then REMOVED entirely after wallet_sendCalls couldn't recognize the sub-account factory
- Files deleted: lib/ensure-donor-wallet.ts, app/api/payments/donor-wallet/*
- DonateModal copy updated to reflect single-wallet recipient
- donation_wallet_address column ignored in code path (still in DB, harmless)
- Smart Wallet–only enforcement preserved via smartWalletOnly preference + useIsWalletACoinbaseSmartWallet hook
- Outcome: every recipient lookup just reads users.wallet_address — clean, predictable, recipient-routing works for follows + donations + tier subs
Interactive paymaster-burn estimator: gas runs ~9% of DOLLA's revenue cut at every scale point.
- Sliders for total users, % verified, % sovereign, ops/user/mo per tier, $/op
- Live monthly ops + gas burn + DOLLA revenue (49% of premium tiers) + net + gas/revenue ratio
- Scale milestones table (1K → 10M users) with same assumptions applied — confirms gas does not eat us alive
- Boundary slider: drop verified % to 1, push free ops to 15 → margin flips negative; tells us where to throttle free-user sponsorship
Sovereign users can send custom $ donations to Sovereign creators — one-time or recurring.
- Donate $ button on every creator profile (next to Follow), opens DonateModal with eligibility probe
- One-time path: OnchainKit <Transaction> → USDC transfer → server verifies on-chain Transfer log, records payment
- Recurring path: subscribe() spend permission → /api/payments/donate/confirm → first charge → cron auto-renews monthly or weekly
- Recipient is always the creator's Donor Wallet (separate from their primary spending wallet)
- Cron updated to handle target_type='donation' alongside creator follows + platform tiers — same recurring engine, three rails
- Migration 00017: 'donation' added to subscription_target_type enum + donation_wallet_address column on users
$4.99/mo Verified and $4.99/wk Sovereign tiers wired end-to-end on Base mainnet.
- Stripe rail removed from tier subs — Coinbase Smart Wallet exclusive
- /api/payments/subscribe + /confirm + /retry-charge endpoints with full recurring spend-permission flow
- Charges route to PLATFORM_WALLET_ADDRESS (corp Coinbase Business USDC-on-Base sink, 0x26b2…7A95)
- Tier-integrity guarantee: users.premium_tier only flips when on-chain charge confirms — no badge without payment
- /api/payments/verify-tier endpoint with self-heal: resets premium_tier to 'free' if no completed payment backs it
- UpgradeModal redesigned: portal-rendered, body-scroll-locked, true fullscreen takeover; centered on every screen size
- FundCard inline auto-fund — when first charge fails for insufficient USDC, modal swaps to Coinbase Onramp without re-asking for a signature
Production switched off testnet. Real USDC, real users, real revenue.
- All env vars updated: NEXT_PUBLIC_BASE_CHAIN_ID=8453, all paymaster URLs flipped from /base-sepolia/ to /base/
- All 500 seeded creator wallets pointed at the corp Coinbase USDC-on-Base sink so all simulated traffic settles to a real account
- Daily cron at /api/cron/charge-subscriptions auto-detects mainnet via chainId — no code changes needed for live billing
- First on-chain probe confirmed: paymaster returns chainId 0x2105, both EntryPoint v0.6 + v0.7 supported, SpendPermissionManager deployed
Two-wallet auto-creation flow, EOA gracefully blocked with a 60-second walkthrough.
- Wagmi config locked to a single connector: coinbaseWallet({ preference: 'smartWalletOnly' }) with classic display — exactly one popup, always the passkey UI
- EOA detection via connector identity (not eth_getCode) so counterfactual Smart Wallets are recognized correctly
- /settings/wallet rebuilt as the Coinbase onboarding hub: 6-tile benefits showcase (USDC APY, Coinbase Card cashback, 0% fees, gasless gas, passkey security, regulated bank), plus a 5-step collapsible walkthrough
- Fund-your-wallet guidance card auto-renders when balance < $1, with three numbered funding paths and a copyable address
- Send / Cash Out card (gasless USDC transfer to any wallet) with explicit "DOLLA never holds your funds" framing + Coinbase deposit address recipe
- All FollowModal + ConnectWalletButton + DonateModal use OnchainKit's <ConnectWallet> — no raw CoinbaseWalletSDK anywhere in app code
Catches the "I have USDC but it's on the wrong network" footgun before it costs anyone money.
- /api/wallet/verify endpoint: shape validation, on-chain reachability, EOA-vs-contract detection, USDC + ETH + tx-count read
- Cross-network USDC discovery — parallel probes to Ethereum L1, Polygon, Arbitrum, Optimism — surfaces "you have $X USDC on Ethereum L1, but this address has 0 on Base"
- Idempotent inline send via OnchainKit <Transaction>, gasless via paymaster
- Settings/Wallet auto-runs verify on every connect + replaces the slow round-trip with wagmi's useReadContract for instant USDC balance display (cached, refetches in background)
Power users can paste a Coinbase Business / exchange deposit address directly.
- 0x[a-fA-F0-9]{40} format validation on the client
- Connected state shows Change/Disconnect controls — no need to drop the whole link to swap addresses
- Burn-address placeholder bug fixed (collided with a real user's wallet); seeded test users cleared so the address is uniquely owned
Six phases of work deployed to production. Public URL, real code, real rails.
- All CDP + Base Account credentials synced to Vercel (11 env vars)
- Production chain set to Base Sepolia (chainId 84532) for safe beta testing
- /test/sponsor diagnostic page live for debugging paymaster sponsorship
- /api/cron/charge-subscriptions ready — scheduled daily at 09:00 UTC
Monthly billing runs itself. Unfollow actually stops the money.
- Vercel Cron daily job finds due subscriptions and charges them via CDP Server Wallet
- 3-day grace period before marking expired; payments roll current_period_end forward
- Unfollow endpoint marks follow inactive + subscription canceled — cron respects status
- Optimistic UI with rollback on server error
A real human used the Follow button. A real dollar moved between two different wallets.
- Follower → creator: $1.00 USDC transferred atomically via Base Account spend permission
- Follow + subscription + payment rows persisted to Supabase
- Two fake wallets also tested bidirectionally: alice ↔ bob, each way, gasless
- First Mint (referral credit) recorded for a creator
Replaced a custom smart contract plan with Base's native spend permissions.
- Installed @coinbase/cdp-sdk, @coinbase/onchainkit, @base-org/account
- Provisioned DOLLA subscription-owner wallet on CDP Server Wallets
- Paymaster + bundler wired end-to-end; gasless UX confirmed live
- Saved weeks of custom Solidity work + audit cost
TypeScript strict, webhook persistence, admin guard, gamification — all real now.
- 47 TypeScript errors fixed; strict mode enabled in production build
- Coinbase + Stripe webhook handlers now persist payments/follows/subscriptions to DB
- Achievement unlock logic wired — 5 tracks with real stats, calling cards, notifications
- Admin route auth guard + Sovereign-tier gate on donations + auth on feed interactions
2026-04-15 – 2026-04-22
SHIPPEDSmooth reveals, passwordless recovery, rock-solid wallet connect.
- First real on-chain follow plumbing ($1 USDC follower → creator)
- Coinbase Smart Wallet integration refactored 4 times until stable
- GPU-composited mobile animations, anticipatory scroll preload
- Logout, forgot password, loading states, null guards, privacy + terms pages
710 users, 1020 posts, 2529 follows. No more mock data.
- PostgreSQL 17 via Supabase, 8 migrations, 20+ tables, RLS on everything
- Google OAuth + email/password auth with auto-created profiles
- Three feeds wired (Discover, Following, Trending) + trending edge function
- Creator dashboard, profile, search, settings — all pulling live data
- Design system locked: glass morphism, teal accent, Outfit + JetBrains Mono
From blank repo to working skeleton in a day.
- Turborepo + pnpm workspaces: apps/web, apps/mobile, 7 shared packages
- Next.js 16 App Router, Tailwind 4, TypeScript strict
- Full database schema designed: users, creators, follows, subscriptions, payments, achievements, mints, charities, notifications
- Dual-rail payment package scaffolded (Coinbase Base + Stripe)
The vision: every follower is a dollar, every dollar reaches the creator, 51% back to the world.
- BMAC validated the thesis ($4.9M ARR, 1M creators, tip-jar model)
- DOLLA evolves it: Follow = $1/mo, 0% platform fee, USDC on Base, 51% of premium revenue donated
- Three-page creator architecture (Free / Monthly / Weekly)
- Interactive pitch site + 18-slide investor deck + research brief
- Brand identity, paradigm-shifter principles, infrastructure plan