Skip to content

Add Audience Hub: centralized contacts, consent, ingest, and GitHub PR installer - #52

Merged
ralyodio merged 2 commits into
masterfrom
feat/audience-hub
Jun 12, 2026
Merged

ralyodio merged 2 commits into
masterfrom
feat/audience-hub

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Summary

Implements the Audience Hub PRD MVP: a centralized, deduplicated, consent-aware contact list across all connected properties, fed by the existing stats.js footprint plus a new trusted server-side ingest, with an owner-initiated GitHub Create PR installer.

Note: this branch stacks on feat/org-audience-and-viewer-role (d6b28e6, org mass-email + org viewer role), so that commit is included here too.

Schema (20260612120000_audience_hub.sql — already applied to the hosted Supabase project)

  • audience_contacts — deduped by normalized email per scope (project's org, else owner; two partial unique indexes), lifecycle status, marketing consent, unsubscribe/suppression, first/last-touch attribution, tags/metadata
  • audience_identities, audience_project_links, audience_events, audience_consent_events
  • project_api_keys — cpk_… server ingest keys, sha256+pepper at rest (mirrors lib/sp/apiToken.ts)
  • project_pr_runs gains kind audience_hub

Pipeline (lib/audience/hub.ts)

Resolve/create contact → upgrade-only status ladder (lead → subscriber → user → customer) with terminal unsubscribe/suppress/delete overriding upgrades → explicit-only consent (account emails never auto-subscribed; suppression beats opt-ins) → identity + project-link upserts → event + consent audit rows.

Ingest

  • stats.js: callable window.crawlproof with track/identify/consent/alias, UTM capture, async stub queue drain; legacy crawlproof.track(name, target) unchanged
  • /api/track forwards identity/lead/consent events fire-and-forget (plain pageviews stay out of audience tables)
  • New POST /api/events: bearer project keys, zod validation, 600/min per-key rate limit, real 202/400/401/429 responses

Dashboard

  • /audience — counts, search, contact table, CSV export (plus consented-only export that excludes unsubscribed/suppressed)
  • /audience/[contactId] — attribution, projects, identities, event timeline, consent history
  • Project Audience tab — install status, API key management, manual snippets, Create PR flow

GitHub installer (lib/github/install-audience.ts)

Stack detection (Next app/pages, Vite, Hono, Express, static), reuses the tracker installer's snippet discovery/injection, generated server/client helper + .env.example docs, opens a reviewable PR on crawlproof/audience-hub-* branches. Owner-initiated only, audit-logged via project_pr_runs, never pushes to default branches.

Docs: docs/audience-hub.md. Deferred per PRD: Resend sync, advanced segments, Supabase backfill into the contact graph, CoinPay DID identities.

Test plan

  • npm run typecheck clean
  • npm test — 341 passed (14 new Audience Hub tests; stats.js contract tests updated for the new callable API)
  • Migration applied to hosted Supabase (supabase db push) — additive only
  • Smoke: identify a user via window.crawlproof("identify", …) on a live property and confirm the contact appears under /audience

🤖 Generated with Claude Code

ralyodio and others added 2 commits June 11, 2026 16:34
Org audience / mass email:
- Connect each project's backing DB (Supabase via service-role auth.users or
  a public table; Turso via @libsql/client read-only SELECT) as a per-org data
  source, sync every user email into a deduped org audience, and blast it
  through the org's existing SMTP/Resend sender config.
- Always adds one-click unsubscribe + List-Unsubscribe headers and skips both
  per-org unsubscribes and globally-unsubscribed marketing_contacts.
- Fix: an explicit "resend" org sender config is no longer hijacked by a global
  SMTP_HOST env (sendOutreachEmail now honors the provider choice).
- Migration 20260611140000_org_audience.sql; new lib/audience/*; dashboard
  panels; tests for connector normalization + query guard.

Org-level viewer role:
- Read-only org members ("viewer"): see every project in the org but cannot
  mutate any project or org data. Reads widened in is_org_wide_member +
  is_project_member; writes unchanged (is_org_owner / is_project_editor still
  owner/member only). Migration 20260611150000_org_member_viewer_role.sql.
- Team-members panel gets a Member/Read-only invite dropdown, a
  "Make read-only / Make editor" toggle, and viewer badges; role carried
  through organization_invitations.role and applied on accept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ller

Implements the Audience Hub PRD MVP:

- Schema: audience_contacts (deduped by normalized email per org/owner
  scope), audience_identities, audience_project_links, audience_events,
  audience_consent_events, project_api_keys (hashed cpk_ keys), and an
  'audience_hub' kind for project_pr_runs.
- Pipeline (lib/audience/hub.ts): resolve/create contact, upgrade-only
  lifecycle ladder with terminal unsubscribe/suppress/delete, explicit-only
  consent, first/last-touch attribution, identity + project-link upserts.
- stats.js: callable window.crawlproof with track/identify/consent/alias,
  UTM capture, async stub queue drain; legacy track(name, target) intact.
- Ingest: /api/track forwards identity/lead/consent events (fire-and-
  forget); new POST /api/events with bearer project keys, zod validation,
  per-key rate limiting, real status codes.
- Dashboard: /audience hub (counts, search, CSV export incl. consented-only
  view), /audience/[contactId] detail, and a project Audience tab with
  install status, API key management, snippets, and the Create PR flow.
- GitHub installer (lib/github/install-audience.ts): stack detection
  (Next app/pages, Vite, Hono, Express, static), reuses tracker snippet
  injection, generated server/client helper + .env.example docs, opens a
  reviewable PR on crawlproof/audience-hub-* branches; audit-logged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

vu1nz Security Review

0 finding(s) in PR #?

No security issues found.

@ralyodio
ralyodio merged commit ee9062f into master Jun 12, 2026
8 checks passed
@ralyodio
ralyodio deleted the feat/audience-hub branch June 12, 2026 14:47
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