Skip to content

feat: PostHog analytics + waitlist with Resend#100

Merged
neonwatty merged 34 commits intomainfrom
feat/posthog-waitlist
Mar 21, 2026
Merged

feat: PostHog analytics + waitlist with Resend#100
neonwatty merged 34 commits intomainfrom
feat/posthog-waitlist

Conversation

@neonwatty
Copy link
Copy Markdown
Collaborator

Summary

  • PostHog integration with consent-first tracking (memory persistence by default, upgrade on cookie accept)
  • Waitlist flow gated by ALLOWED_EMAILS — non-allowed emails see a waitlist form instead of signup
  • Waitlist entries stored in Supabase (waitlist table) + synced to Resend segment
  • Confirmation email sent via Resend on waitlist signup
  • Landing page analytics — tracks page views, scroll depth, CTA clicks, and waitlist conversions
  • Cookie consent banner — accept/decline with PostHog persistence upgrade

New files (13)

  • src/lib/cookie-consent.ts + test — consent state with localStorage + CustomEvent
  • src/lib/analytics.ts + test — 8 client-side tracking functions
  • src/lib/analytics-server.ts + test — server-side PostHog via posthog-node
  • src/components/PostHogProvider.tsx — consent-based PostHog init
  • src/components/PostHogWrapper.tsx — SSR-disabled dynamic wrapper
  • src/components/ui/CookieConsent.tsx — cookie consent banner
  • src/app/(auth)/signup/actions.tsjoinWaitlist server action
  • src/app/(auth)/signup/WaitlistForm.tsx — waitlist form component
  • src/lib/email/templates/waitlist-confirmation.tsx — email template
  • supabase/migrations/00031_add_waitlist.sql — waitlist table + RLS

Modified files (7)

  • src/app/providers.tsx — PostHogWrapper + CookieConsent
  • src/app/(auth)/signup/page.tsx — beta gate logic
  • src/app/landing-page.tsx — landing analytics
  • src/components/Landing/Hero.tsx — CTA click tracking
  • src/lib/supabase/middleware-utils.ts — CSP for PostHog
  • src/lib/email/send.tssendWaitlistConfirmation()
  • src/types/database.tsDbWaitlist interface

New env vars (set in Vercel)

  • NEXT_PUBLIC_POSTHOG_KEY
  • NEXT_PUBLIC_POSTHOG_HOST
  • RESEND_WAITLIST_AUDIENCE_ID

Test plan

  • TypeScript strict — no type errors
  • 1522 tests passing (3 new test files)
  • Knip — no unused exports
  • ESLint — no new errors
  • Manual: verify cookie consent banner appears on landing page
  • Manual: verify waitlist form shows on /signup when ALLOWED_EMAILS is set
  • Manual: verify waitlist submission creates entry in Supabase + Resend segment
  • Manual: verify PostHog events appear in dashboard

🤖 Generated with Claude Code

neonwatty and others added 30 commits March 20, 2026 10:31
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…, types

Fixes 8 issues from spec review:
- Migration 00009 → 00031 (correct sequence)
- Server action moved to src/app/(auth)/signup/actions.ts (colocated)
- CSP update for PostHog host documented
- Beta gate clarified: allowed emails can still reach signup form
- WaitlistForm colocated with signup page
- shouldSendEmail() exclusion documented
- Server-side PostHog client (analytics-server.ts) specified
- DbWaitlist type addition to database.ts documented

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed and fixed: reordered analytics before PostHog provider,
CSP reads env var instead of hardcoding, removed dead code,
added test env cleanup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Also patches vitest setup and config to handle Node 25's broken built-in
localStorage (requires --localstorage-file) with an in-memory fallback,
and sets jsdom URL to http://localhost for correct environment init.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… audienceId

Resend migrated from audiences to segments. Use contacts.create with
segments array instead of the deprecated audienceId parameter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Split signup page into SignupPage (coordinator) + SignupForm (form UI)
to stay under the 150-line function limit.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…llision

Two files shared the 00027 prefix causing a unique constraint violation
on schema_migrations_pkey during supabase db reset in CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase CLI requires numeric-only prefixes — the 00027b rename was
skipped with "file name must match pattern". Merged the unique active
checkin index into 00027_rate_limits.sql and removed the duplicate file.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase takes ~5.5min to start all containers in CI. The previous
timeout of 90×3s=270s was insufficient now that migrations run
successfully (the duplicate 00027 collision was masking this).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CLI 2.67.1 sends migrations as a batch transaction, causing
"relation does not exist" when a later migration references a table
created by an earlier one in the same batch. 2.78.1 applies migrations
sequentially. Cache key updated to pull fresh Docker images.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase CLI applies migrations with search_path that may not include
"public". Unqualified table names like "notes" fail with "relation does
not exist". Fixed all trigger DDL in 00028 to use public.* prefix.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
supabase start batches all migrations into a single transaction, causing
"relation does not exist" errors when later migrations reference tables
from earlier ones. Use --ignore-health-check for start, then db reset
to apply migrations one-by-one in separate transactions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
supabase start batches migrations in a single transaction. If that
fails (cross-migration table references), fall back to db reset which
applies them sequentially. Removed --ignore-health-check to keep
services running properly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase CLI sends all migrations as a single batch transaction.
Cross-file DDL dependencies (e.g., CREATE TABLE in 00004 + CREATE
TRIGGER ON that table in 00028) fail because Postgres can't resolve
forward references within a batch. Squashing into one file eliminates
inter-file dependencies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase CLI parses all migration statements before executing any.
Cross-file DDL (CREATE TRIGGER/POLICY/INDEX on tables from earlier
migrations) fails at parse time with "relation does not exist".

Fixed 14 migration files by wrapping cross-file DDL in DO blocks
with EXECUTE to defer table name resolution to execution time.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase CLI sends the seed file as a batch and pre-parses all
statements. The explicit BEGIN/COMMIT wrapper may conflict with the
batch transaction model and cause table resolution failures.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
neonwatty and others added 4 commits March 21, 2026 10:09
Supabase CLI batch-parses the seed.sql and fails resolving table names
before execution. Disable seed in config.toml and apply it directly via
psql after supabase start, where statements execute sequentially.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supabase CLI sends all migration files as a single batch, pre-parsing
all SQL before executing any. This fails when migrations reference
tables created in earlier files (e.g., CREATE TRIGGER ON a table from
another migration).

Fix: disable seed in config.toml, restore original migration files,
and apply both migrations and seed sequentially via psql in CI where
each file executes independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
supabase start successfully applies migrations (they work in batch mode
since tables + policies are in the same file). Only the seed needs psql
because it references tables from migration files and fails in batch parse.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…tion

The enforce_couple_resource_limit() function uses SET search_path = ''
for security but then queries TG_TABLE_NAME without schema qualification.
This fails because "notes" is not found without "public." prefix when
search_path is empty. Use TG_TABLE_SCHEMA.TG_TABLE_NAME instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@neonwatty neonwatty merged commit 3daa459 into main Mar 21, 2026
4 of 6 checks passed
@neonwatty neonwatty deleted the feat/posthog-waitlist branch March 21, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant