Claude Cursor GitHub Copilot opencode Skill

observability-and-growth

Full instrumentation from day one. PostHog consolidates product analytics + feature flags + error tracking (one platform, one bill). GA4 via GTM (14-step automation, custom dimensions over events, server-side tagging). Sentry (deep error tracking + performance). Stripe (webhook-f

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download heymegabyte-claude-skills-13-observability-and-growth-e7acb91.zip · 43 KB
Part of heymegabyte/claude-skills — 18 skills

Install

skills CLI npx skills add https://github.com/heymegabyte/claude-skills/tree/master/13-observability-and-growth
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install heymegabyte-claude-skills@llmmart
Git git clone https://github.com/heymegabyte/claude-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole heymegabyte/claude-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

13 — Observability and Growth

Tiered PostHog+Sentry+GA4 stacks wired from day one; feature flags, PLG patterns, incident auto-remediation.

Instrumentation tiers

Per _kernel/standards.md#integrations:

  • Tier 1 (solo) — PostHog + Workers Tracing OTLP (2 vendors max, cookie-free, free tier covers <10k MAU)
  • Tier 2 (enterprise) — + Sentry @sentry/cloudflare v9 + GA4/GTM + Axiom
  • Tier 3 (LLM-heavy >10k calls/mo) — + AI Gateway

PostHog (Tier 1 cornerstone)

  • Snippet on every HTML page w/ persistence:'memory' (cookie-free); capture_pageview + capture_pageleave + autocapture:true
  • Unified platform: product analytics + feature flags + session replay + error tracking
  • CSP: script-src + connect-src for posthog domain
  • Per-feature event naming: <feature>:<action> (signup:complete, editor:save, share:copy)

Sentry (Tier 2)

  • @sentry/cloudflare v9 + withSentry wrapper; project via mcp__sentry__create_project (org:megabyte-labs)
  • SENTRY_DSN via wrangler secret put
  • Pattern: withSentry(env => ({ dsn, tracesSampleRate: 1.0, sendDefaultPii: false }), worker)
  • Breadcrumbs before risky ops; capture exception w/ context tags (worker | route | userId)
  • Release tracking via SENTRY_RELEASE env; Workers Tracing handles I/O spans

Workers Tracing (Tier 1 + 2) / GA4 + GTM (Tier 2) / AI Gateway (Tier 3)

  • Workers Tracing: [observability] enabled = true in wrangler.jsonc — zero-config OTel I/O tracing; export to Axiom, Honeycomb, Grafana, Datadog via @opentelemetry/exporter-trace-otlp-http
  • GA4 + GTM: container snippet (head script + noscript iframe); CSP: googletagmanager.com + google-analytics.com + analytics.google.com + region1.google-analytics.com; server-side tagging for EU; custom dimensions over custom events
  • AI Gateway: env.AI.run() auto-routes; direct Anthropic: https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/anthropic/v1/messages; caching + rate-limit + fallback + per-call logging

Stripe (SaaS billing only — per rules/payments-routing.md)

  • Webhook-first w/ idempotent processing (D1 dedupe table payment_events(event_id, source, processed_at) UNIQUE)
  • Stripe-Signature HMAC + 5-min replay window
  • Mint products + prices via MCP (idempotent via lookup_key); subscription state machine in D1
  • STRIPE_WEBHOOK_SECRET via POST /v1/webhook_endpoints

Square (accept-money default — per rules/payments-routing.md)

  • Square Web Payments SDK card form + Apple Pay + Google Pay + Cash App Pay
  • Square-Signature HMAC-SHA256 w/ 6-hr replay window; idempotency_key UUID per request (24-hr dedupe)
  • Nonprofit verified-501c3 discount (2.6%+10¢ vs 3.5%+15¢)

Listmonk (newsletter — self-hosted on Coolify)

  • Amazon SES SMTP relay (LISTMONK_FROM_EMAIL); listmonkSendTx(env, { templateAlias, ... }) via KV-cached alias→id map
  • Templates in emails/*.html synced via scripts/listmonk-sync.mjs
  • Auth: Authorization: token <user>:<key> (Listmonk 3.x API-user pattern)

PLG 7-Layer Framework

  1. Discovery — SEO + AI search + word-of-mouth + paid
  2. Sign-up — passwordless preferred (Clerk M2M JWT)
  3. Activation — first-value-in-X-min metric (aha moment per rules/feature-flags.md instrumentation)
  4. Engagement — DAU/MAU ratio, session depth
  5. Retention — D1/D7/D30 cohort
  6. Revenue — upgrade trigger, expansion
  7. Referral — viral coefficient, two-sided rewards

Programmatic SEO (5 page types)

  • /integrations/{tool} · /compare/{a}-vs-{b} · /for/{audience} · /templates/{type} · /{city}-{service}
  • Each: unique H1 + meta desc + 800+ unique words + 1 unique image + 3+ internal links + 1+ outbound citation. Cap 200 pages per axis.
  • Per rules/copy-writing.md § pSEO + rules/thin-source-amplification.md.

GEO / AI search

  • Quotable answer blocks 40-60 words (LLM citation magnet); FAQPage schema highest AI-citation rate (ChatGPT / Perplexity / Google AI Overviews)
  • JSON-LD facts MUST also appear as visible HTML body text; lead paragraphs answer query in <40 words
  • EEAT: author bio + Person schema + sameAs + dated revision + ownership statement
  • llms.txt at site root (DX-only, <0.3% adoption — not build gate)

Local-business conversions + CRO

Track per local-conversions.md: phone_click (tel:) · direction_click (Google Maps) · form_submit · booking_click (Calendly/Cal.com) · chat_click · review_click (Google Business/Yelp). Each fires PostHog + Sentry breadcrumb + (Tier 2) GA4 conversion event.

CRO: sticky CTA bar on mobile · scroll-progress bar (subtle) · exit-intent modal (cart/pricing only, not blogs) · social proof near every CTA · urgency without dark patterns ("3 spots left this week" if true) · trust strip above fold · single primary CTA per surface.

Incident auto-remediation

Sentry → Inngest pipeline: (1) Sentry webhook on event.alert.triggered → (2) Inngest dispatches incident-responder agent → (3) agent reads event, traces to source file, proposes fix, opens PR via gh MCP → (4) PR auto-merges if all CI gates pass per rules/ai-seniority.md.

See submodules: posthog, ga4-gtm, sentry, stripe-billing, square-payments, listmonk, plg-framework, programmatic-seo, incident-remediation, geo-ai-search, local-conversions.

Files (claude-skills)
  • activation-funnel.md 11.1 KB
    ---
    name: "Activation Funnel"
    description: "PostHog event taxonomy for PLG SaaS funnels: first-magic-moment definition, 5-stage activation funnel (visit→signup→core-action→second-session→paid), cohort SQL, milestone tracking, in-app upgrade prompts without dark patterns."
    updated: "2026-06-18"
    always-load: false
    ---
    
    # Activation Funnel
    
    PLG lives and dies on activation. Define ONE north-star activation metric — the moment a user experiences undeniable value — then instrument everything upstream and downstream of it.
    
    ## Magic-Moment Definition
    
    The magic moment = the single action after which paid conversion probability jumps >3× above baseline. Pick it empirically:
    
    1. Pull cohorts of users who converted to paid in the last 90 days.
    2. Run `correlation_score` across every `feature_activated` event against `subscription_started` within 14 days.
    3. The event with highest correlation AND >30% frequency IS your magic moment (e.g. `project_first_published`, `integration_connected`, `report_generated`).
    4. Hard-code as `MAGIC_MOMENT_EVENT = 'your_event'` in `worker/activation.ts`; never change without repeating this analysis.
    
    ## 5-Stage Funnel
    
    ```
    visit → signup → core_action → second_session → paid
    ```
    
    | Stage | Definition | Target Conversion |
    |-------|-----------|------------------|
    | `visit` | `page_viewed` on any marketing page | — |
    | `signup` | `signup_completed` (account created) | ≥3% visit→signup |
    | `core_action` | Magic-moment event fired ≤24h after signup | ≥40% signup→core |
    | `second_session` | Session started ≥24h after signup | ≥30% core→second |
    | `paid` | `subscription_started` or `purchase_completed` | ≥8% second→paid |
    
    If any stage is below target, that stage owns the sprint — not the one after it.
    
    ## PostHog Event Taxonomy
    
    ### Acquisition Events
    
    ```typescript
    // Every marketing page load
    posthog.capture('page_viewed', {
      path: window.location.pathname,
      title: document.title,
      referrer: document.referrer,
      utm_source: params.get('utm_source'),
      utm_campaign: params.get('utm_campaign'),
    })
    
    // CTA engagement
    posthog.capture('cta_clicked', {
      cta_text: el.innerText,
      cta_location: 'hero' | 'pricing' | 'nav' | 'footer',
      destination: el.href,
    })
    ```
    
    ### Activation Events
    
    ```typescript
    // Account created
    posthog.capture('signup_completed', {
      method: 'google' | 'email' | 'github',
      plan: 'free',
      referral_code: code ?? null,
    })
    
    // First-run milestone (fire once, idempotently)
    posthog.capture('onboarding_step_completed', {
      step: 'profile' | 'invite' | 'integration' | 'first_action',
      step_index: 1 | 2 | 3 | 4,
      time_since_signup_s: Math.floor((Date.now() - signupTs) / 1000),
    })
    
    // Magic moment
    posthog.capture(MAGIC_MOMENT_EVENT, {
      context: 'onboarding' | 'organic',
      time_since_signup_s: elapsed,
    })
    
    // Identify at signup + enrich on magic moment
    posthog.identify(userId, {
      email: user.email,
      plan: 'free',
      signup_method: method,
    })
    posthog.group('company', orgId, { name: org.name, plan: org.plan })
    ```
    
    ### Engagement Events
    
    ```typescript
    posthog.capture('feature_activated', { feature_name: 'csv_export', context: 'dashboard' })
    posthog.capture('session_started', { session_number: n, days_since_signup: d })
    posthog.capture('search_performed', { query_length: q.length, results_count: n })
    posthog.capture('error_displayed', { error_type: 'validation' | 'server', page: path })
    ```
    
    ### Revenue Events
    
    ```typescript
    posthog.capture('upgrade_prompt_shown', { trigger: 'feature_gate' | 'usage_limit' | 'milestone', plan_suggested: 'pro' })
    posthog.capture('pricing_page_viewed', { source: 'nav' | 'upgrade_prompt' | 'direct' })
    posthog.capture('checkout_started', { plan: 'pro', billing: 'monthly' | 'annual' })
    posthog.capture('subscription_started', { plan: 'pro', billing: 'annual', mrr: 29 })
    posthog.capture('subscription_cancelled', { plan: 'pro', reason: surveyAnswer, days_active: n })
    ```
    
    ## PostHog Funnel Queries
    
    ### Full Activation Funnel (HogQL)
    
    ```sql
    SELECT
      countIf(event = 'page_viewed')                              AS visits,
      countIf(event = 'signup_completed')                         AS signups,
      countIf(event = 'project_first_published')                  AS magic_moment,
      countIf(event = 'session_started' AND properties.session_number >= 2) AS second_session,
      countIf(event = 'subscription_started')                     AS paid,
      round(countIf(event = 'signup_completed') * 100.0
            / nullIf(countIf(event = 'page_viewed'), 0), 2)      AS visit_to_signup_pct,
      round(countIf(event = 'project_first_published') * 100.0
            / nullIf(countIf(event = 'signup_completed'), 0), 2) AS signup_to_magic_pct,
      round(countIf(event = 'subscription_started') * 100.0
            / nullIf(countIf(event = 'session_started'
              AND properties.session_number >= 2), 0), 2)        AS second_to_paid_pct
    FROM events
    WHERE timestamp >= now() - interval 30 day
    ```
    
    ### Time-to-Activation Cohort (HogQL)
    
    ```sql
    -- Distribution of minutes to magic moment, segmented by signup method
    SELECT
      person.properties.signup_method AS method,
      histogram(
        toUInt32(dateDiff('minute',
          minIf(timestamp, event = 'signup_completed'),
          minIf(timestamp, event = 'project_first_published')
        )),
        10
      ) AS activation_time_histogram
    FROM events
    WHERE event IN ('signup_completed', 'project_first_published')
      AND timestamp >= now() - interval 90 day
    GROUP BY method
    ```
    
    ### Weekly Activation Rate Trend
    
    ```sql
    SELECT
      toMonday(timestamp)                                          AS week,
      countIf(event = 'signup_completed')                         AS new_signups,
      countIf(event = 'project_first_published')                  AS activated,
      round(countIf(event = 'project_first_published') * 100.0
            / nullIf(countIf(event = 'signup_completed'), 0), 1) AS activation_rate_pct
    FROM events
    WHERE timestamp >= now() - interval 12 week
    GROUP BY week
    ORDER BY week
    ```
    
    ### Users Who Signed Up But Never Hit Magic Moment (D1 equivalent for Workers)
    
    ```sql
    -- D1 query — find stale free users for nudge campaign
    SELECT u.id, u.email, u.created_at,
           MAX(e.occurred_at) AS last_event,
           julianday('now') - julianday(u.created_at) AS days_since_signup
    FROM users u
    LEFT JOIN events e ON e.user_id = u.id
    WHERE u.plan = 'free'
      AND u.magic_moment_at IS NULL
      AND julianday('now') - julianday(u.created_at) BETWEEN 1 AND 7
    GROUP BY u.id
    ORDER BY days_since_signup DESC
    LIMIT 500
    ```
    
    ## Cohort Analysis: Retention by Activation Status
    
    In PostHog UI: Retention → breakdown by `$feature/activated` (custom property set on magic-moment).
    
    Workers-side: set a PostHog person property at magic-moment:
    
    ```typescript
    await posthog.groupIdentify({ groupType: 'company', groupKey: orgId,
      properties: { activated: true, activated_at: new Date().toISOString() } })
    ```
    
    Target: activated cohort retains 2× better at 7-day / 30-day.
    
    ## First-Run Experience (FRE) Patterns
    
    ### Onboarding Checklist (Worker-backed, D1-persisted)
    
    ```typescript
    // worker/onboarding.ts
    const STEPS: OnboardingStep[] = [
      { key: 'profile',      label: 'Complete your profile',     required: true  },
      { key: 'integration',  label: 'Connect your first tool',   required: true  },
      { key: 'first_action', label: 'Create your first project', required: true  },
      { key: 'invite',       label: 'Invite a teammate',         required: false },
    ]
    
    // Persist completion to D1 + fire PostHog event
    export async function markStep(env: Env, userId: string, step: string) {
      await env.DB.prepare(
        `INSERT OR IGNORE INTO onboarding_completions (user_id, step, completed_at)
         VALUES (?, ?, datetime('now'))`
      ).bind(userId, step).run()
      await posthog.capture({ distinctId: userId, event: 'onboarding_step_completed',
        properties: { step, completed_all: await allStepsDone(env, userId) } })
    }
    ```
    
    ### Empty States as Action Prompts
    
    Every empty state is an activation hook — never "No data yet":
    
    ```tsx
    // ✅
    <EmptyState
      icon={<FolderIcon />}
      title="Create your first project"
      body="Projects are where your work lives. Most teams activate in under 2 minutes."
      cta={<Button onClick={createProject}>Create project</Button>}
    />
    // ❌ never:
    <p>No projects yet.</p>
    ```
    
    ### Progress Indicator
    
    Show milestone language: "2 of 4 steps to full access." Not a percentage bar (users abandon bars).
    
    ## In-App Upgrade Prompts (Anti-Dark-Pattern)
    
    ### Triggers (when to show)
    
    | Trigger | Event | Condition |
    |---------|-------|-----------|
    | Feature gate | `feature_gate_hit` | User clicks locked feature |
    | Usage limit | `usage_limit_approached` | ≥80% of free quota consumed |
    | Milestone | `team_member_invited` | Free plan hits seat limit |
    | Value moment | Magic-moment event | User just hit the magic moment (positive state) |
    
    Never show:
    
    - On first session before magic moment
    - More than once per 48h per trigger type
    - After a payment error
    - In the middle of a user flow
    
    ### Prompt Shape
    
    ```tsx
    <UpgradePrompt
      trigger="feature_gate"           // logged to PostHog
      feature="csv_export"
      headline="Export your data"      // value-forward, not fear
      body="CSV export is on Pro. All your data, your way."
      cta="Upgrade to Pro"
      dismissible                      // ALWAYS dismissible
      onUpgrade={() => posthog.capture('upgrade_prompt_accepted', { trigger, feature })}
      onDismiss={() => posthog.capture('upgrade_prompt_dismissed', { trigger, feature })}
    />
    ```
    
    ### Suppression Logic (Worker-side)
    
    ```typescript
    async function shouldShowUpgradePrompt(env: Env, userId: string, trigger: string): Promise<boolean> {
      const row = await env.DB.prepare(
        `SELECT shown_at FROM upgrade_prompts_shown
         WHERE user_id = ? AND trigger = ?
         ORDER BY shown_at DESC LIMIT 1`
      ).bind(userId, trigger).first<{ shown_at: string }>()
      if (!row) return true
      const hoursSince = (Date.now() - new Date(row.shown_at).getTime()) / 3_600_000
      return hoursSince >= 48
    }
    ```
    
    ## Milestone Tracking (D1 Schema)
    
    ```sql
    CREATE TABLE IF NOT EXISTS user_milestones (
      user_id       TEXT NOT NULL,
      milestone     TEXT NOT NULL,  -- 'signup','magic_moment','second_session','paid','churned'
      achieved_at   TEXT NOT NULL DEFAULT (datetime('now')),
      metadata      TEXT,           -- JSON blob
      PRIMARY KEY (user_id, milestone)
    );
    
    CREATE TABLE IF NOT EXISTS upgrade_prompts_shown (
      id         INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id    TEXT NOT NULL,
      trigger    TEXT NOT NULL,
      feature    TEXT,
      shown_at   TEXT NOT NULL DEFAULT (datetime('now')),
      dismissed  INTEGER NOT NULL DEFAULT 0,
      converted  INTEGER NOT NULL DEFAULT 0
    );
    ```
    
    ## Activation Dashboard (PostHog)
    
    Build one insight per funnel stage + pin to "Activation" dashboard:
    
    1. **Funnel chart** — 5 stages, weekly trend, segment by signup method
    2. **Retention table** — activated vs. not, 7/14/30 day columns
    3. **Time-to-activation histogram** — p50/p90 in minutes
    4. **Upgrade prompt conversion rate** — by trigger type
    5. **Magic-moment correlation heatmap** — which events precede paid conversion
    
    ## See
    
    - `analytics-configuration` — PostHog init + standard events
    - `feature-flags-and-experiments` — A/B test upgrade prompts
    - `conversion-optimization` — landing page + CTA patterns
    - `stripe-billing` / `square-payments` — subscription_started → revenue
    
  • analytics-configuration.md 7.2 KB
    ---
    name: "Analytics Configuration"
    description: "Canonical owner of GA4 setup (14-step automation), GTM container configuration, PostHog SDK integration, feature flags, A/B tests, event taxonomy, funnel definitions, session recording config, and scroll/engagement tracking. The intelligence layer that powers data-driven decisions."
    updated: "2026-04-23"
    always-load: false
    ---
    
    # Analytics Configuration
    
    ## Stack
    
    - **Tag Management** — GTM (Google, not self-hosted)
    - **Web Analytics** — GA4 (Google, not self-hosted)
    - **Product Analytics** — PostHog (self-hosted, posthog.megabyte.space)
    - **Feature Flags** — PostHog (self-hosted)
    - **A/B Testing** — PostHog Experiments (self-hosted)
    - **Session Recording** — PostHog with masked inputs (self-hosted)
    - **Error Tracking** — Sentry (***AUTO-PROVISION***, sentry.megabyte.space)
    
    ## Event Naming
    
    `object_action` (snake_case, past tense): `page_viewed`, `cta_clicked`, `form_submitted`, `signup_completed`, `subscription_started`, `error_displayed`.
    
    ## Standard Events
    
    - **`page_viewed`** — props: `path, title, referrer`; trigger: every page
    - **`scroll_depth_reached`** — props: `percent (25/50/75/100)`; trigger: scroll milestones
    - **`cta_clicked`** — props: `cta_text, cta_location, destination`; trigger: CTA click
    - **`form_submitted`** — props: `form_name, success, error_type`; trigger: form submit
    - **`signup_completed`** — props: `method, plan`; trigger: account created
    - **`purchase_completed`** — props: `amount, currency, plan`; trigger: checkout success
    - **`feature_activated`** — props: `feature_name, context`; trigger: first use
    - **`search_performed`** — props: `query, results_count`; trigger: site search
    - **`error_displayed`** — props: `error_type, page, message`; trigger: error shown
    
    ## Rules
    
    1. GTM is ONLY script loader (except Sentry early init)
    2. Never hardcode GA4/PostHog in HTML — load via GTM
    3. Self-hosted PostHog preferred (no cookies, no GDPR banner)
    4. Every action: GA4 event + PostHog event + Sentry breadcrumb
    5. Feature flags BEFORE building feature. Ship behind flag. Remove after 100% rollout.
    6. A/B tests require hypothesis + min sample size + defined success metric
    7. Session recording masks all inputs by default. PII never recorded.
    8. DataLayer pushes happen before action completes (optimistic)
    9. No double-counting events across tools
    10. Scroll depth: 25/50/75/100% milestones only
    
    ## GTM Snippet
    
    ```html
    <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-XXXXXXX');</script>
    <!-- noscript iframe after <body> -->
    ```
    
    ## PostHog Init
    
    ```typescript
    posthog.init('phc_PROJECT_KEY', {
      api_host: 'https://posthog.megabyte.space',
      capture_pageview: true, capture_pageleave: true, autocapture: true,
      session_recording: { maskAllInputs: true, maskTextSelector: '.sensitive' },
      persistence: 'memory', // No cookies
      loaded: (ph) => { if (import.meta.env.DEV) ph.opt_out_capturing(); },
    });
    ```
    
    ## Feature Flags
    
    ```typescript
    function isFeatureEnabled(flag: string, defaultValue = false): boolean {
      return posthog?.isFeatureEnabled(flag) ?? defaultValue;
    }
    // With payload: posthog.getFeatureFlagPayload('hero-experiment')
    ```
    
    ## A/B Testing
    
    Define: `name`, `hypothesis`, `variants`, `successMetric`, `minimumSampleSize`, `duration`.
    
    ```typescript
    const variant = posthog.getFeatureFlag('pricing-page-layout');
    ```
    
    Track conversion with experiment + variant properties.
    
    ## Scroll Depth
    
    Track 25/50/75/100% milestones. Push to `dataLayer` + `posthog.capture`. Use passive scroll listener.
    
    ## DataLayer Pattern
    
    ```typescript
    function trackEvent(event: string, props: Record<string, unknown> = {}) {
      window.dataLayer = window.dataLayer || [];
      window.dataLayer.push({ event, ...props });
      posthog.capture(event, props);
    }
    ```
    
    ## Funnels
    
    ```typescript
    const FUNNELS = {
      signupToActivation: { steps: ['page_viewed /signup', 'signup_completed', 'feature_activated'], window: '7d' },
      visitToPurchase: { steps: ['page_viewed /', 'cta_clicked pricing', 'purchase_completed'], window: '30d' },
    };
    ```
    
    ## CSP for Analytics
    
    ```
    script-src: https://www.googletagmanager.com https://www.google-analytics.com https://*.posthog.com
    connect-src: https://www.google-analytics.com https://analytics.google.com https://*.posthog.com https://*.sentry.io
    ```
    
    ## Full Analytics Auto-Provision (***MANDATORY — ALL THREE***)
    
    ### Sentry (Error Tracking + Performance)
    
    Every project gets Sentry from day one. Missing `@sentry/cloudflare` (Workers) or `@sentry/node` → install SDK + wrap entry point + create project via `mcp__sentry__create_project` (org: megabyte-labs, team: megabyte-labs, platform: javascript) → set `SENTRY_DSN` via `wrangler secret put`. Full-stack traces: `tracesSampleRate`, `app.onError()` → `captureException` with route + userId tags, breadcrumbs before risky ops, `SENTRY_RELEASE` for deploy tracking.
    
    ### PostHog (Product Analytics + Feature Flags + Session Recording)
    
    Every HTML page gets PostHog snippet. Config:
    
    - `persistence:'memory'` (cookie-free, no GDPR banner)
    - `capture_pageview:true`
    - `capture_pageleave:true`
    - `autocapture:true`
    
    CSP: script-src + connect-src for PostHog API host. PostHog project key stored as env var `POSTHOG_KEY` or inline for static HTML. Self-hosted preferred (posthog.megabyte.space) but `us.i.posthog.com` acceptable for cloud.
    
    Events: `page_viewed`, `cta_clicked`, `form_submitted`, `donate_click`, `newsletter_signup`, `scroll_depth`.
    
    ### GA4/GTM (Marketing Analytics + Tag Management)
    
    Every HTML page gets GTM container (head script + noscript iframe after body). GTM container ID: `get-secret GTM_CONTAINER_ID` (Megabyte Labs). CSP:
    
    - `script-src` — googletagmanager.com + google-analytics.com
    - `connect-src` — analytics.google.com + region1.google-analytics.com
    - `img-src` — googletagmanager.com + google-analytics.com
    
    GA4 configured inside GTM (never standalone). Custom dimensions over separate events. 14-month retention. Google Signals enabled.
    
    ### Enforcement
    
    Missing ANY of the three → add in same prompt. No page ships without all three firing. Every action: GA4 event + PostHog event + Sentry breadcrumb. CSP must allow all three domains. Verify in browser DevTools Network tab before marking deploy as done.
    
    ## Verification Checklist
    
    - GTM loads
    - GA4 fires (DebugView)
    - PostHog captures pageviews (Live Events)
    - Sentry DSN set + errors captured (Sentry Issues dashboard)
    - Custom events fire on interactions
    - Feature flags resolve
    - Session recording captures
    - No duplicate events
    - CSP allows all domains
    - Scroll depth fires at milestones
    - No cookies set (self-hosted PostHog)
    
    ## Ownership
    
    - **Owns** — GA4 config, GTM setup, PostHog SDK, feature flags, A/B tests, event taxonomy, funnels, session recording, scroll tracking, dataLayer architecture, cookie-free analytics, verification
    - **Never owns** — Error tracking config (→ 13 Sentry), payments (→ 18), email marketing (→ 13 Listmonk), growth strategy (→ 13), deployment (→ 08), SEO (→ 28)
    
  • conversion-optimization.md 5 KB
    ---
    name: "Conversion Optimization"
    description: "CRO patterns for SaaS signup flows, pricing pages, paywalls, and churn prevention"
    version: "2.0.0"
    updated: "2026-04-23"
    ---
    
    # Conversion Optimization
    
    ## Core Loop
    
    Measure → hypothesis → test → analyze → iterate. Never skip baseline measurement. A/B test ONE variable at a time via PostHog feature flags.
    
    **Statistical significance:** min 1000 visitors/variant, 95% confidence, 2-week minimum duration.
    
    ## Key Metrics
    
    - **Conversion Rate** — `signups / visitors` — target >3% SaaS, >5% free tier
    - **Trial-to-Paid** — `paid / trial starts` — target >25%
    - **Monthly Churn** — `churned / start MRR` — target <5%
    - **LTV** — `ARPU / churn rate` — target >3x CAC
    - **CAC** — `total spend / new customers` — target <LTV/3
    - **Activation** — `users hitting aha moment / signups` — target >40%
    
    ## Pricing Page Patterns
    
    - 3-tier layout (Free | Pro $50/mo | Enterprise custom)
    - Highlight recommended tier with brand cyan `#00E5FF` border
    - Annual toggle prominent (20% discount = $480/yr vs $600)
    - Show savings amount, not just percentage
    - Feature comparison table with checkmarks
    - Social proof: "Join 2,000+ teams" with real numbers
    - FAQ section below tiers addressing objections
    - Mobile: stack vertically, recommended first
    
    ## Signup Flow
    
    - Reduce fields to email + password (name optional, collect later)
    - Social auth (Google, GitHub) above form
    - Progress indicator for multi-step
    - Inline validation on blur
    - Smart defaults for settings
    - Skip onboarding option
    - **Time-to-value** — user sees core feature within 60 seconds of signup
    
    ## Paywall Patterns
    
    - **Soft paywall** — show feature, blur/lock result, explain what Pro unlocks
    - **Usage-based triggers** — `"You've used 8 of 10 free reports. Upgrade for unlimited."`
    - **Trial** — 14-day default, no credit card required upfront
    - **Email sequence** — day 1 welcome, day 7 value, day 12 urgency
    - Never hard-lock features without showing value first
    
    ## Churn Prevention
    
    - **Cancellation flow** — survey (too expensive | missing feature | switching | other) → offer alternatives (pause, downgrade, discount) → confirm
    - **Win-back** — automated email at 7/30/90 days via Resend
    - **Usage decline** — PostHog event tracking, alert when weekly active drops 50%
    - **Dunning** — 3 retries over 7 days, Stripe Smart Retries enabled
    
    ## Form Optimization
    
    - Inline validation (not on submit)
    - Autofill attributes on all fields
    - Smart defaults reduce decisions
    - **Error messages** — specific + actionable (`"Email already registered. Sign in?"` not `"Error 409"`)
    - **Success** — brief + next (`"Account created. Let's set up your first project →"`)
    - **Loading** — contextual (`"Creating your workspace..."` not `"Loading..."`)
    
    ## Anti-Patterns
    
    Never:
    
    - Dark patterns (hidden costs, forced continuity, misdirection)
    - Fake urgency
    - Guilt-tripping cancel copy
    - Hiding unsubscribe
    - Pre-checked upsells
    - Bait-and-switch pricing
    
    **ADA/WCAG** — all conversion elements keyboard accessible, screen reader compatible, sufficient contrast.
    
    ## Local Business Conversions (***NOT SAAS***)
    
    Local businesses don't have trial-to-paid funnels. Their conversions are physical-world actions.
    
    ### Event Taxonomy (PostHog + GA4 + Sentry breadcrumb)
    
    **Primary (highest intent):**
    
    - `phone_click` — `tel:` link clicked
    - `direction_click` — Google Maps directions clicked
    - `form_submit` — Contact/booking form submitted
    - `booking_click` — External booking CTA (OpenTable, Calendly, etc.)
    
    **Secondary:**
    
    - `email_click` — `mailto:` link clicked
    - `chat_open` — Live chat widget opened
    - `review_click` — "Leave a Review" CTA clicked
    
    **Micro:**
    
    - `menu_download` — PDF menu/brochure downloaded
    - `coupon_claim` — Special offer clicked
    - `social_click` — Social media profile link clicked
    
    ### GA4 Goals (auto-configure via GTM dataLayer)
    
    ```javascript
    // Inject in every local business site build
    document.querySelectorAll('a[href^="tel:"]').forEach(el =>
      el.addEventListener('click', () => gtag('event', 'phone_click', { phone: el.href }))
    );
    document.querySelectorAll('a[href*="maps.google"]').forEach(el =>
      el.addEventListener('click', () => gtag('event', 'direction_click'))
    );
    ```
    
    ### Local Funnel (PostHog)
    
    `Visit → Engagement (scroll 50%+) → Micro (menu/gallery) → Macro (call/directions/form/booking)`
    
    Track weekly conversion rate. Alert if rate drops >20% WoW. Typical local business: 3-8% macro conversion rate (vs 1-3% SaaS).
    
    ### Call Tracking (optional premium)
    
    - Google forwarding number (free with Google Ads) OR CallRail ($45/mo)
    - Tracks: duration, caller location, recording, missed call alerts
    - Without call tracking: `tel:` click count as proxy
    
    ## Tools
    
    - **PostHog** — funnel analysis + feature flags + session replay
    - **Stripe** — billing portal, proration, Smart Retries, revenue recovery
    - **Resend** — transactional + win-back emails
    - **Inngest** — automated lifecycle workflows (trial → paid → churn sequences)
    
  • email-marketing-and-listmonk.md 8.1 KB
    ---
    name: "Email Marketing and Listmonk"
    description: "Listmonk on Coolify for newsletters and campaigns. REST API for subscriber management and campaign creation, Go template system, webhook integration with Hono for subscription events, double opt-in flow, unsubscribe handling, and Neon PostgreSQL backend."
    updated: "2026-04-23"
    ---
    
    # Email Marketing and Listmonk
    
    ## Coolify Deployment
    
    ```yaml
    # Listmonk on Coolify: Docker compose
    # Image: listmonk/listmonk:latest
    # Database: Neon PostgreSQL (external)
    # Domain: listmonk.megabyte.space (proxied via CF)
    # Env vars:
    #   LISTMONK_app__address: "0.0.0.0:9000"
    #   LISTMONK_app__admin_username: (from Coolify secrets)
    #   LISTMONK_app__admin_password: (from Coolify secrets)
    #   LISTMONK_db__host: (Neon host)
    #   LISTMONK_db__port: 5432
    #   LISTMONK_db__user: (Neon user)
    #   LISTMONK_db__password: (Neon password)
    #   LISTMONK_db__database: listmonk
    #   LISTMONK_db__ssl_mode: require
    ```
    
    ## Hono Worker Proxy (CF Worker → Listmonk)
    
    ```typescript
    // src/routes/newsletter.ts
    import { Hono } from 'hono';
    import { zValidator } from '@hono/zod-validator';
    import { z } from 'zod';
    
    const newsletter = new Hono<{ Bindings: Env }>();
    
    const listmonkFetch = async (env: Env, path: string, options?: RequestInit) => {
      const auth = btoa(`${env.LISTMONK_USER}:${env.LISTMONK_PASS}`);
      return fetch(`${env.LISTMONK_URL}/api${path}`, {
        ...options,
        headers: {
          ...options?.headers,
          Authorization: `Basic ${auth}`,
          'Content-Type': 'application/json',
        },
      });
    };
    
    // Subscribe (public endpoint — Turnstile protected)
    newsletter.post('/subscribe', zValidator('json', z.object({
      email: z.string().email(),
      name: z.string().min(1).max(100).optional(),
      listIds: z.array(z.number()).default([1]), // default newsletter list
      turnstileToken: z.string(),
    })), async (c) => {
      const { email, name, listIds, turnstileToken } = c.req.valid('json');
    
      // Verify Turnstile
      const turnstileRes = await fetch('https://challenges.cloudflare.com/turnstile/v0/siteverify', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ secret: c.env.TURNSTILE_SECRET, response: turnstileToken }),
      });
      const turnstile = await turnstileRes.json() as { success: boolean };
      if (!turnstile.success) return c.json({ error: 'Verification failed' }, 400);
    
      // Create subscriber (double opt-in)
      const res = await listmonkFetch(c.env, '/subscribers', {
        method: 'POST',
        body: JSON.stringify({
          email,
          name: name || email.split('@')[0],
          status: 'enabled',
          lists: listIds,
          preconfirm_subscriptions: false, // requires double opt-in confirmation
        }),
      });
    
      if (!res.ok) {
        const err = await res.json() as { message: string };
        if (err.message?.includes('already exists')) {
          return c.json({ message: 'Already subscribed' });
        }
        return c.json({ error: 'Subscription failed' }, 500);
      }
    
      return c.json({ message: 'Check your email to confirm' });
    });
    
    // Unsubscribe
    newsletter.post('/unsubscribe', zValidator('json', z.object({
      email: z.string().email(),
    })), async (c) => {
      const { email } = c.req.valid('json');
    
      // Find subscriber
      const search = await listmonkFetch(c.env, `/subscribers?query=subscribers.email='${encodeURIComponent(email)}'`);
      const { data } = await search.json() as { data: { results: Array<{ id: number }> } };
      if (!data.results?.length) return c.json({ message: 'Unsubscribed' });
    
      // Blocklist subscriber
      await listmonkFetch(c.env, `/subscribers/${data.results[0].id}/blocklist`, { method: 'PUT' });
      return c.json({ message: 'Unsubscribed' });
    });
    
    // List available newsletter lists (public)
    newsletter.get('/lists', async (c) => {
      const res = await listmonkFetch(c.env, '/lists?page=1&per_page=50');
      const { data } = await res.json() as { data: { results: Array<{ id: number; name: string; description: string }> } };
      return c.json({ lists: data.results.map(({ id, name, description }) => ({ id, name, description })) });
    });
    
    export { newsletter };
    ```
    
    ## Campaign Creation via API
    
    ```typescript
    // Create and send a campaign programmatically
    async function createCampaign(env: Env, opts: {
      name: string;
      subject: string;
      body: string;
      listIds: number[];
      templateId?: number;
      sendAt?: string; // ISO date for scheduled send
    }): Promise<number> {
      const res = await listmonkFetch(env, '/campaigns', {
        method: 'POST',
        body: JSON.stringify({
          name: opts.name,
          subject: opts.subject,
          body: opts.body,
          content_type: 'richtext',
          lists: opts.listIds,
          template_id: opts.templateId || 1,
          type: 'regular',
          tags: ['automated'],
          send_at: opts.sendAt,
        }),
      });
      const { data } = await res.json() as { data: { id: number } };
    
      // Start the campaign
      await listmonkFetch(env, `/campaigns/${data.id}/status`, {
        method: 'PUT',
        body: JSON.stringify({ status: 'running' }),
      });
    
      return data.id;
    }
    ```
    
    ## Go Template System (Listmonk)
    
    ```html
    <!-- Listmonk uses Go's text/template syntax -->
    <!-- Available variables: .Subscriber, .Campaign, .UnsubscribeURL, .TrackLink -->
    
    <!-- Campaign template -->
    <h1>{{ .Campaign.Subject }}</h1>
    <p>Hey {{ .Subscriber.FirstName }},</p>
    
    {{ .Campaign.Body }}
    
    <!-- Conditional content by subscriber attribute -->
    {{ if eq .Subscriber.Attribs.plan "pro" }}
      <p>As a Pro member, you get early access.</p>
    {{ else }}
      <p><a href="{{ TrackLink "https://example.com/upgrade" }}">Upgrade to Pro</a> for early access.</p>
    {{ end }}
    
    <!-- Tracked link -->
    <a href="{{ TrackLink "https://example.com/feature" }}">Check out our new feature</a>
    
    <!-- Unsubscribe (REQUIRED) -->
    <p><a href="{{ .UnsubscribeURL }}">Unsubscribe</a></p>
    
    <!-- Custom template with brand styling -->
    <!-- Base template wraps all campaigns with header/footer/brand colors -->
    ```
    
    ## Webhook Integration (Listmonk → Hono)
    
    Listmonk webhook events:
    
    - `subscriber.created`, `subscriber.modified`, `subscriber.deleted`
    - `campaign.sent`, `campaign.updated`
    
    ```typescript
    // src/routes/webhooks/listmonk.ts
    
    const listmonkWebhook = new Hono<{ Bindings: Env }>();
    
    listmonkWebhook.post('/listmonk', async (c) => {
      // Verify webhook secret
      const secret = c.req.header('X-Listmonk-Secret');
      if (secret !== c.env.LISTMONK_WEBHOOK_SECRET) {
        return c.json({ error: 'Invalid secret' }, 401);
      }
    
      const event = await c.req.json();
    
      switch (event.event) {
        case 'subscriber.created':
          // Sync to CRM, PostHog, etc.
          // posthog.capture('newsletter_signup', { email: event.data.email });
          break;
        case 'subscriber.modified':
          if (event.data.status === 'blocklisted') {
            // Track unsubscribe
            // posthog.capture('newsletter_unsubscribe', { email: event.data.email });
          }
          break;
        case 'campaign.sent':
          // Track campaign completion
          break;
      }
    
      return c.json({ received: true });
    });
    
    export { listmonkWebhook };
    ```
    
    ## Double Opt-In Flow
    
    1. User submits email → `POST /api/newsletter/subscribe`
    2. Listmonk creates subscriber (status: `unconfirmed`)
    3. Listmonk sends confirmation email with unique link
    4. User clicks confirmation link → status: `confirmed`
    5. Subscriber receives future campaigns
    6. No confirmation within 72h → auto-cleanup (Listmonk setting)
    
    ## Footer Newsletter Component
    
    ```html
    <!-- Minimal newsletter signup for site footer -->
    <form (submit)="subscribe($event)" class="newsletter-form">
      <input type="email" [(ngModel)]="email" placeholder="your@email.com" required />
      <button type="submit" [disabled]="submitting">Subscribe</button>
      <cf-turnstile [siteKey]="turnstileSiteKey" (resolved)="onTurnstile($event)" />
      @if (message) { <p class="feedback">{{ message }}</p> }
    </form>
    ```
    
    ## Listmonk API Quick Reference
    
    - `GET  /api/subscribers?query=...&page=1&per_page=50` — search subscribers
    - `POST /api/subscribers` — create subscriber
    - `PUT  /api/subscribers/:id` — update subscriber
    - `PUT  /api/subscribers/:id/blocklist` — blocklist (unsubscribe)
    - `GET  /api/lists` — list all lists
    - `POST /api/campaigns` — create campaign
    - `PUT  /api/campaigns/:id/status` — start/pause/cancel campaign
    - `GET  /api/campaigns/:id` — campaign details + stats
    - `POST /api/tx` — send transactional email
    
  • feature-flags-and-experiments.md 6.6 KB
    ---
    name: "Feature Flags and Experiments"
    description: "PostHog feature flags and A/B testing. Server-side flag evaluation in Hono middleware, Angular directive for client-side flags, experiment setup with control/variants and statistical significance, gradual rollout patterns, and kill switches."
    updated: "2026-04-23"
    ---
    
    # Feature Flags and Experiments
    
    ## PostHog Server-Side Flags (Hono Middleware)
    
    ```typescript
    // src/middleware/feature-flags.ts
    import { PostHog } from 'posthog-node';
    import type { Context, Next } from 'hono';
    
    function createPostHog(env: Env): PostHog {
      return new PostHog(env.POSTHOG_API_KEY, { host: 'https://posthog.megabyte.space', flushAt: 1, flushInterval: 0 });
    }
    
    // Middleware: evaluate flags for authenticated user
    async function featureFlagMiddleware(c: Context<{ Bindings: Env }>, next: Next): Promise<void> {
      const userId = c.get('userId');
      if (!userId) { await next(); return; }
    
      const posthog = createPostHog(c.env);
      const flags = await posthog.getAllFlags(userId, {
        personProperties: { email: c.get('userEmail'), plan: c.get('userPlan') },
      });
    
      c.set('featureFlags', flags);
      await posthog.shutdown();
      await next();
    }
    
    // Route-level flag check
    async function requireFlag(flagName: string, c: Context): Promise<boolean> {
      const flags = c.get('featureFlags') as Record<string, boolean | string>;
      return !!flags[flagName];
    }
    
    // Usage in route
    app.get('/api/new-feature', featureFlagMiddleware, async (c) => {
      if (!await requireFlag('new-dashboard', c)) {
        return c.json({ error: 'Feature not available' }, 404);
      }
      // Feature implementation
      return c.json({ data: 'new feature response' });
    });
    ```
    
    ## Angular Feature Flag Directive
    
    ```typescript
    // feature-flag.directive.ts
    import { Directive, Input, TemplateRef, ViewContainerRef, OnInit, DestroyRef, inject } from '@angular/core';
    import { FeatureFlagService } from './feature-flag.service';
    
    @Directive({ selector: '[appFeatureFlag]', standalone: true })
    export class FeatureFlagDirective implements OnInit {
      @Input('appFeatureFlag') flagName = '';
      @Input('appFeatureFlagElse') elseTemplate?: TemplateRef<unknown>;
    
      private destroyRef = inject(DestroyRef);
    
      constructor(
        private templateRef: TemplateRef<unknown>,
        private viewContainer: ViewContainerRef,
        private flagService: FeatureFlagService,
      ) {}
    
      ngOnInit(): void {
        this.flagService.isEnabled(this.flagName).subscribe((enabled) => {
          this.viewContainer.clear();
          if (enabled) {
            this.viewContainer.createEmbeddedView(this.templateRef);
          } else if (this.elseTemplate) {
            this.viewContainer.createEmbeddedView(this.elseTemplate);
          }
        });
      }
    }
    
    // feature-flag.service.ts
    import { Injectable, signal } from '@angular/core';
    import { Observable, from, of } from 'rxjs';
    import { map, shareReplay } from 'rxjs/operators';
    import posthog from 'posthog-js';
    
    @Injectable({ providedIn: 'root' })
    export class FeatureFlagService {
      private cache = new Map<string, Observable<boolean>>();
    
      isEnabled(flag: string): Observable<boolean> {
        if (!this.cache.has(flag)) {
          this.cache.set(flag, of(posthog.isFeatureEnabled(flag) ?? false).pipe(shareReplay(1)));
        }
        return this.cache.get(flag)!;
      }
    
      getVariant(flag: string): string | boolean | undefined {
        return posthog.getFeatureFlag(flag);
      }
    
      /** Force reload all flags (after login/plan change) */
      reload(): void {
        posthog.reloadFeatureFlags();
        this.cache.clear();
      }
    }
    
    // Template usage:
    // <div *appFeatureFlag="'new-dashboard'; else oldDashboard">New UI</div>
    // <ng-template #oldDashboard>Old UI</ng-template>
    ```
    
    ## A/B Experiment Setup
    
    PostHog Dashboard: **Experiments → New Experiment**
    
    1. **Name** — `"pricing-page-redesign"`
    2. **Feature flag key** — `"pricing-page-variant"`
    3. **Variants** — control (50%), test-a (25%), test-b (25%)
    4. **Goal metric** — `upgrade_click` event
    5. **Secondary** — `time_on_page` property
    6. **Minimum sample** — 1000 per variant
    7. **Significance threshold** — 95%
    
    Client implementation:
    
    ```typescript
    const variant = posthog.getFeatureFlag('pricing-page-variant');
    // variant: 'control' | 'test-a' | 'test-b'
    
    switch (variant) {
      case 'test-a':
        showRedesignedPricing();
        break;
      case 'test-b':
        showMinimalPricing();
        break;
      default:
        showOriginalPricing();
    }
    
    // Track conversion (automatically attributed to variant)
    posthog.capture('upgrade_click', { plan: 'pro', source: 'pricing-page' });
    ```
    
    ## Gradual Rollout Pattern
    
    PostHog Dashboard: **Feature Flags → New Flag**
    
    ### Release conditions
    
    1. **Start** — 5% of users → monitor errors/feedback
    2. **Day 2** — 25% → check metrics
    3. **Day 5** — 50% → validate at scale
    4. **Day 7** — 100% → full release
    
    ### Targeting
    
    - **By property** — `plan = 'pro'` (beta users first)
    - **By cohort** — `"internal-team"` → 100%, everyone else → rollout %
    - **By email** — `*@megabyte.space` → always on (internal testing)
    
    ### Rollout automation via API
    
    ```typescript
    async function updateRollout(flagKey: string, percentage: number, env: Env): Promise<void> {
      await fetch(`https://posthog.megabyte.space/api/projects/${env.POSTHOG_PROJECT_ID}/feature_flags`, {
        method: 'PATCH',
        headers: {
          Authorization: `Bearer ${env.POSTHOG_PERSONAL_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          key: flagKey,
          filters: { groups: [{ rollout_percentage: percentage }] },
        }),
      });
    }
    ```
    
    ## Kill Switch Pattern
    
    Critical flags that disable features instantly. Name convention: `kill-{feature}` (inverted logic).
    
    - `kill-payments` → true = payments **DISABLED**
    - `kill-signups` → true = signups **DISABLED**
    
    ```typescript
    async function checkKillSwitch(feature: string, c: Context): Promise<boolean> {
      const flags = c.get('featureFlags') as Record<string, boolean>;
      return flags[`kill-${feature}`] === true;
    }
    
    // In route
    app.post('/api/checkout', async (c) => {
      if (await checkKillSwitch('payments', c)) {
        return c.json({ error: 'Payments temporarily unavailable', code: 'MAINTENANCE' }, 503);
      }
      // Normal checkout flow
    });
    ```
    
    PostHog: toggle `kill-payments` to true → instant disable. No deploy needed. Rolls back in <1 second.
    
    ## Experiment Analysis Checklist
    
    1. Minimum 100 conversions per variant (not just impressions)
    2. Run for full week minimum (captures day-of-week effects)
    3. Check for novelty effect (flat after initial spike = novelty)
    4. Statistical significance ≥ 95% before calling winner
    5. Check segments: does variant win across all plans/devices?
    6. Document: hypothesis, variants, metric, result, decision
    7. Archive losing variant code. Ship winner. Clean up flag.
    
  • mcp-server-registry.md 15.1 KB
    ---
    name: "MCP Server Registry"
    description: "Health monitoring + drift detection for Brian's 15+ MCP servers (CF, GitHub, Stripe, Bitwarden, Gmail, Slack, etc.). Registry JSON shape, daily healthcheck Worker, tool-list drift detection, Slack/email alert on degradation, secret rotation reminders."
    updated: "2026-06-18"
    always-load: false
    ---
    
    # MCP Server Registry
    
    Every MCP server is a dependency. Treat it like one: registry, healthchecks, drift detection, rotation schedule. A silently-broken MCP is worse than a missing one — Claude calls the tool and gets garbage instead of an error.
    
    ## Registry Schema
    
    Canonical registry lives at `~/.claude/mcp-registry.json`. One object per server:
    
    ```jsonc
    // ~/.claude/mcp-registry.json
    {
      "servers": [
        {
          "id": "cloudflare",
          "name": "Cloudflare Developer Platform",
          "package": "@cloudflare/mcp-server-cloudflare",
          "transport": "stdio",
          "secret_keys": ["CLOUDFLARE_API_TOKEN"],
          "secret_rotation_days": 90,
          "tier": "load-bearing",          // load-bearing | supporting | experimental
          "healthcheck_url": null,          // null for stdio; URL for SSE/HTTP servers
          "known_tools": [                  // snapshot of advertised tools — drift-checked daily
            "d1_database_query", "kv_namespace_get", "workers_list",
            "r2_bucket_create", "search_cloudflare_documentation"
          ],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy",              // healthy | degraded | unreachable | unknown
          "notes": "CF API token scope: D1, KV, R2, Workers, SSL for SaaS"
        },
        {
          "id": "github",
          "name": "GitHub",
          "package": "@modelcontextprotocol/server-github",
          "transport": "stdio",
          "secret_keys": ["GITHUB_TOKEN"],
          "secret_rotation_days": 365,
          "tier": "load-bearing",
          "known_tools": [
            "create_pull_request", "get_file_contents", "push_files",
            "search_code", "list_commits", "create_issue", "merge_pull_request"
          ],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "stripe",
          "name": "Stripe",
          "package": "stripe-mcp",
          "transport": "stdio",
          "secret_keys": ["STRIPE_API_KEY"],
          "secret_rotation_days": 180,
          "tier": "load-bearing",
          "known_tools": [
            "stripe_api_read", "stripe_api_write", "fetch_stripe_resources",
            "search_stripe_resources", "create_refund"
          ],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "bitwarden",
          "name": "Bitwarden",
          "package": "@bitwarden/mcp-server",
          "transport": "stdio",
          "secret_keys": ["BW_CLIENTID", "BW_CLIENTSECRET", "BW_PASSWORD"],
          "secret_rotation_days": 180,
          "tier": "load-bearing",
          "known_tools": ["list", "get", "create_item", "edit_item", "generate", "status"],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "gmail",
          "name": "Gmail",
          "package": "@modelcontextprotocol/server-gmail",
          "transport": "stdio",
          "secret_keys": ["GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "GOOGLE_REFRESH_TOKEN"],
          "secret_rotation_days": 60,      // OAuth refresh tokens expire; rotate aggressively
          "tier": "supporting",
          "known_tools": ["search_threads", "get_thread", "create_draft", "list_labels"],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "slack",
          "name": "Slack",
          "package": "@modelcontextprotocol/server-slack",
          "transport": "stdio",
          "secret_keys": ["SLACK_BOT_TOKEN", "SLACK_TEAM_ID"],
          "secret_rotation_days": 365,
          "tier": "supporting",
          "known_tools": [
            "slack_send_message", "slack_read_channel", "slack_search_public",
            "slack_read_user_profile", "slack_search_channels"
          ],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "posthog",
          "name": "PostHog",
          "package": "@posthog/mcp-server",
          "transport": "stdio",
          "secret_keys": ["POSTHOG_API_KEY"],
          "secret_rotation_days": 365,
          "tier": "supporting",
          "known_tools": ["exec", "query", "list_insights", "list_feature_flags"],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "neon",
          "name": "Neon",
          "package": "@neondatabase/mcp-server-neon",
          "transport": "stdio",
          "secret_keys": ["NEON_API_KEY"],
          "secret_rotation_days": 365,
          "tier": "supporting",
          "known_tools": ["run_sql", "create_branch", "list_projects", "describe_table_schema"],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "resend",
          "name": "Resend",
          "package": "@resend/mcp-server",
          "transport": "stdio",
          "secret_keys": ["RESEND_API_KEY"],
          "secret_rotation_days": 365,
          "tier": "supporting",
          "known_tools": ["send-email", "create-contact", "list-audiences", "get-domain"],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "playwright",
          "name": "Playwright",
          "package": "@playwright/mcp",
          "transport": "stdio",
          "secret_keys": [],
          "secret_rotation_days": null,
          "tier": "load-bearing",
          "known_tools": [
            "browser_navigate", "browser_click", "browser_snapshot", "browser_take_screenshot",
            "browser_fill", "browser_evaluate", "browser_network_requests"
          ],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "sentry",
          "name": "Sentry",
          "package": "@sentry/mcp-server",
          "transport": "stdio",
          "secret_keys": ["SENTRY_AUTH_TOKEN"],
          "secret_rotation_days": 180,
          "tier": "supporting",
          "known_tools": ["search_issues", "get_sentry_resource", "update_issue", "find_organizations"],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        },
        {
          "id": "upstash",
          "name": "Upstash",
          "package": "@upstash/mcp-server",
          "transport": "stdio",
          "secret_keys": ["UPSTASH_EMAIL", "UPSTASH_API_KEY"],
          "secret_rotation_days": 365,
          "tier": "supporting",
          "known_tools": [
            "redis_database_run_redis_commands", "redis_database_list_databases",
            "qstash_publish_message", "qstash_schedules_list"
          ],
          "last_healthy": "2026-06-18T00:00:00Z",
          "last_checked": "2026-06-18T00:00:00Z",
          "status": "healthy"
        }
      ]
    }
    ```
    
    ## Vendor Tiers
    
    - **load-bearing** — Claude blocks on these for core dev tasks (CF, GitHub, Playwright, Stripe, Bitwarden). SLA: fix within 4h if broken. Never allow >1 load-bearing server degraded simultaneously.
    - **supporting** — adds value but work continues without them (Slack, Gmail, PostHog, Neon, Resend). SLA: fix within 48h.
    - **experimental** — new integrations under evaluation. No SLA.
    
    ## Daily Healthcheck Worker
    
    Deploy at `workers.megabyte.space/mcp-healthcheck` — runs as a Cron Trigger at `0 7 * * *` (7am UTC):
    
    ```typescript
    // workers/mcp-healthcheck/index.ts
    import registry from '../../mcp-registry.json'
    
    interface McpStatus { id: string; status: 'healthy' | 'degraded' | 'unreachable'; latency_ms: number; error?: string }
    
    export default {
      async scheduled(_event: ScheduledEvent, env: Env, ctx: ExecutionContext) {
        ctx.waitUntil(runChecks(env))
      },
      async fetch(req: Request, env: Env) {
        // GET /status — returns last-known state from KV
        const state = await env.MCP_HEALTH_KV.get('state', 'json') as Record<string, McpStatus>
        return Response.json(state ?? {})
      }
    }
    
    async function runChecks(env: Env) {
      const results: McpStatus[] = await Promise.all(
        registry.servers.map(server => checkServer(server, env))
      )
    
      const state = Object.fromEntries(results.map(r => [r.id, r]))
      await env.MCP_HEALTH_KV.put('state', JSON.stringify(state))
    
      const degraded = results.filter(r => r.status !== 'healthy')
      if (degraded.length > 0) await sendAlerts(env, degraded)
    
      await logToR2(env, results)
    }
    
    async function checkServer(server: typeof registry.servers[0], env: Env): Promise<McpStatus> {
      const t0 = Date.now()
      try {
        if (server.healthcheck_url) {
          // HTTP/SSE transport — hit the health endpoint
          const res = await fetch(server.healthcheck_url, { signal: AbortSignal.timeout(5000) })
          return { id: server.id, status: res.ok ? 'healthy' : 'degraded', latency_ms: Date.now() - t0,
                   error: res.ok ? undefined : `HTTP ${res.status}` }
        }
        // stdio transport — verify package exists in npm registry
        const res = await fetch(`https://registry.npmjs.org/${server.package}/latest`,
          { signal: AbortSignal.timeout(5000) })
        return { id: server.id, status: res.ok ? 'healthy' : 'degraded', latency_ms: Date.now() - t0,
                 error: res.ok ? undefined : `npm registry: HTTP ${res.status}` }
      } catch (e) {
        return { id: server.id, status: 'unreachable', latency_ms: Date.now() - t0,
                 error: String(e) }
      }
    }
    ```
    
    ## Drift Detection
    
    Drift = the set of tools a server advertises today differs from `known_tools` in the registry. New tools may unlock capability; removed tools will break prompts.
    
    ```typescript
    // workers/mcp-healthcheck/drift.ts
    // Run weekly via a separate Cron Trigger: "0 8 * * 1" (Monday 8am UTC)
    export async function detectDrift(env: Env) {
      // Fetch live tool manifests by calling each server's list-tools endpoint
      // For npm-hosted servers: pull README + CHANGELOG from GitHub, parse tool names
      const drifts: DriftReport[] = []
    
      for (const server of registry.servers) {
        const liveTools = await fetchLiveToolList(server, env)
        if (!liveTools) continue
    
        const known = new Set(server.known_tools)
        const live = new Set(liveTools)
        const added = liveTools.filter(t => !known.has(t))
        const removed = server.known_tools.filter(t => !live.has(t))
    
        if (added.length > 0 || removed.length > 0) {
          drifts.push({ serverId: server.id, added, removed })
        }
      }
    
      if (drifts.length > 0) await sendDriftAlerts(env, drifts)
      return drifts
    }
    
    async function fetchLiveToolList(server: typeof registry.servers[0], env: Env): Promise<string[] | null> {
      // Heuristic: parse GitHub README for a tools table
      // Packages follow pattern: github.com/org/mcp-server-${name}
      try {
        const pkg = await fetch(`https://registry.npmjs.org/${server.package}/latest`).then(r => r.json<any>())
        const repoUrl = pkg.repository?.url?.replace(/^git\+/, '').replace(/\.git$/, '')
        if (!repoUrl) return null
        const [, owner, repo] = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/) ?? []
        if (!owner) return null
        const readme = await fetch(
          `https://raw.githubusercontent.com/${owner}/${repo}/main/README.md`
        ).then(r => r.text())
        // Extract tool names: lines containing backtick-wrapped identifiers in a tools table
        return [...readme.matchAll(/`([a-z][a-z0-9_]{2,50})`/g)]
          .map(m => m[1])
          .filter(t => !['bash', 'json', 'yaml', 'typescript'].includes(t))
      } catch { return null }
    }
    ```
    
    ## Secret Rotation Reminders
    
    Check rotation schedule daily; alert 14 days before expiry:
    
    ```typescript
    // workers/mcp-healthcheck/rotation.ts
    export async function checkRotationDue(env: Env) {
      const due: RotationAlert[] = []
      const now = Date.now()
    
      for (const server of registry.servers) {
        if (!server.secret_rotation_days || !server.last_healthy) continue
        const lastRotated = new Date(server.last_healthy).getTime() // proxy: use last_healthy
        const rotationDue = lastRotated + server.secret_rotation_days * 86_400_000
        const daysUntilDue = Math.floor((rotationDue - now) / 86_400_000)
    
        if (daysUntilDue <= 14) {
          due.push({
            serverId: server.id,
            secretKeys: server.secret_keys,
            daysUntilDue,
            tier: server.tier,
          })
        }
      }
    
      if (due.length > 0) await sendRotationReminders(env, due)
    }
    ```
    
    ## Alert Delivery
    
    ```typescript
    async function sendAlerts(env: Env, degraded: McpStatus[]) {
      const lines = degraded.map(s =>
        `• *${s.id}* (${registry.servers.find(r => r.id === s.id)?.tier}) — ${s.status}: ${s.error ?? 'no error'}`
      ).join('\n')
    
      // Slack
      await fetch(env.SLACK_WEBHOOK_URL, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          text: `🔴 MCP Health Alert — ${degraded.length} server(s) degraded`,
          blocks: [
            { type: 'section', text: { type: 'mrkdwn', text: `*MCP Server Degradation*\n${lines}` } },
            { type: 'section', text: { type: 'mrkdwn', text: `Check: https://workers.megabyte.space/mcp-healthcheck/status` } },
          ],
        }),
      })
    
      // Email via Resend (load-bearing degradation only)
      const loadBearingDown = degraded.filter(s =>
        registry.servers.find(r => r.id === s.id)?.tier === 'load-bearing'
      )
      if (loadBearingDown.length > 0) {
        await fetch('https://api.resend.com/emails', {
          method: 'POST',
          headers: { Authorization: `Bearer ${env.RESEND_API_KEY}`, 'Content-Type': 'application/json' },
          body: JSON.stringify({
            from: 'alerts@megabyte.space',
            to: 'brian@megabyte.space',
            subject: `URGENT: Load-bearing MCP servers down (${loadBearingDown.map(s => s.id).join(', ')})`,
            html: `<pre>${lines}</pre>`,
          }),
        })
      }
    }
    ```
    
    ## R2 Audit Log
    
    ```typescript
    async function logToR2(env: Env, results: McpStatus[]) {
      const date = new Date().toISOString().slice(0, 10)
      await env.MCP_AUDIT_R2.put(
        `mcp-health/${date}.ndjson`,
        results.map(r => JSON.stringify({ ...r, ts: new Date().toISOString() })).join('\n'),
        { httpMetadata: { contentType: 'application/x-ndjson' } }
      )
    }
    ```
    
    ## wrangler.toml for Healthcheck Worker
    
    ```toml
    name = "mcp-healthcheck"
    main = "workers/mcp-healthcheck/index.ts"
    compatibility_date = "2025-03-01"
    
    [[kv_namespaces]]
    binding = "MCP_HEALTH_KV"
    id = "YOUR_KV_ID"
    
    [[r2_buckets]]
    binding = "MCP_AUDIT_R2"
    bucket_name = "mcp-audit-logs"
    
    [vars]
    SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/..."
    
    [[triggers.crons]]
    crons = ["0 7 * * *", "0 8 * * 1"]  # daily health, weekly drift
    ```
    
    ## Updating the Registry
    
    When you add a new MCP server or a server's tool list changes:
    
    1. Run `npx @modelcontextprotocol/inspector <server-cmd>` to get the live tool list.
    2. Update `known_tools` in `mcp-registry.json`.
    3. Bump `last_healthy` to today.
    4. Commit — the registry is source-of-truth, not a generated artifact.
    
    ## See
    
    - `secret-auto-provisioning` — `get-secret` + Bitwarden integration
    - `secret-provisioning` — rotation cadence by secret class
    - `drift-detection` — broader drift detection doctrine
    - `cloudflare-lock-in-is-leverage` — why health Worker lives on CF
    - `workers-tracing-otlp` — add OTLP traces to healthcheck runs
    
  • sentry-alert-rules.md 8.8 KB
    ---
    name: "Sentry Alert Rules"
    description: "Auto-configure Sentry alert rules at first deploy. Error spike detection, new issue alerts, unhandled rejection tracking, P95 latency monitoring, Slack integration, per-project rules, and deploy silence windows. All via Sentry API."
    updated: "2026-04-23"
    ---
    
    # Sentry Alert Rules
    
    Every deployed project gets four alert rules automatically at first deploy. Per-project (not global) — each domain gets its own rules. All alerts route to Slack `#emdash-alerts`.
    
    ## Alert Rule Set
    
    ### 1. Error Spike (Critical)
    
    Fires when error rate exceeds 5x the normal baseline within a 5-minute window.
    
    ```json
    {
      "name": "[{project}] Error Spike",
      "actionMatch": "all",
      "filterMatch": "all",
      "frequency": 300,
      "conditions": [{
        "id": "sentry.rules.conditions.event_frequency.EventFrequencyPercentCondition",
        "interval": "5m",
        "value": 500,
        "comparisonType": "percent",
        "comparisonInterval": "1h"
      }],
      "actions": [{
        "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
        "workspace": "{slack_workspace_id}",
        "channel": "#emdash-alerts",
        "tags": "environment,release,url"
      }],
      "environment": "production"
    }
    ```
    
    ### 2. New Issue (Low Priority)
    
    First occurrence of any new error. Grouped so repeat triggers don't spam.
    
    ```json
    {
      "name": "[{project}] New Issue",
      "actionMatch": "all",
      "filterMatch": "all",
      "frequency": 1800,
      "conditions": [{
        "id": "sentry.rules.conditions.first_seen_event.FirstSeenEventCondition"
      }],
      "filters": [{
        "id": "sentry.rules.filters.level.LevelFilter",
        "match": "gte",
        "level": "warning"
      }],
      "actions": [{
        "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
        "workspace": "{slack_workspace_id}",
        "channel": "#emdash-alerts",
        "tags": "environment,browser,os"
      }],
      "environment": "production"
    }
    ```
    
    ### 3. Unhandled Rejection (High Priority)
    
    Any unhandled promise rejection or uncaught exception. Immediate notification.
    
    ```json
    {
      "name": "[{project}] Unhandled Rejection",
      "actionMatch": "all",
      "filterMatch": "all",
      "frequency": 60,
      "conditions": [{
        "id": "sentry.rules.conditions.every_event.EveryEventCondition"
      }],
      "filters": [{
        "id": "sentry.rules.filters.tagged_event.TaggedEventFilter",
        "key": "mechanism",
        "match": "eq",
        "value": "unhandledrejection"
      }],
      "actions": [{
        "id": "sentry.integrations.slack.notify_action.SlackNotifyServiceAction",
        "workspace": "{slack_workspace_id}",
        "channel": "#emdash-alerts",
        "tags": "environment,url,release"
      }],
      "environment": "production"
    }
    ```
    
    ### 4. P95 Latency (Warning)
    
    P95 response time exceeds 500ms for 10 consecutive minutes.
    
    ```json
    {
      "name": "[{project}] P95 Latency",
      "dataset": "transactions",
      "aggregate": "p95(transaction.duration)",
      "query": "",
      "timeWindow": 10,
      "triggers": [{
        "label": "critical",
        "alertThreshold": 500,
        "actions": [{
          "type": "slack",
          "targetIdentifier": "#emdash-alerts",
          "integrationId": "{slack_integration_id}"
        }]
      }, {
        "label": "resolved",
        "alertThreshold": 300,
        "actions": [{
          "type": "slack",
          "targetIdentifier": "#emdash-alerts",
          "integrationId": "{slack_integration_id}"
        }]
      }],
      "environment": "production"
    }
    ```
    
    ## API Configuration
    
    All rules created via Sentry API. Base URL: `https://sentry.megabyte.space` (self-hosted).
    
    ### Create Issue Alert
    
    ```bash
    curl -X POST "https://sentry.megabyte.space/api/0/projects/{org}/{project}/rules/" \
      -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{...rule JSON...}'
    ```
    
    ### Create Metric Alert (P95 Latency)
    
    ```bash
    curl -X POST "https://sentry.megabyte.space/api/0/projects/{org}/{project}/alert-rules/" \
      -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{...metric alert JSON...}'
    ```
    
    Issue alerts → `/rules/`. Metric alerts → `/alert-rules/`. Different endpoints.
    
    ## Slack Integration Setup
    
    ```bash
    # 1. Get integration ID (one-time, reuse across projects)
    curl "https://sentry.megabyte.space/api/0/organizations/{org}/integrations/?provider_key=slack" \
      -H "Authorization: Bearer $SENTRY_AUTH_TOKEN"
    
    # 2. Extract integrationId and workspace ID from response
    # 3. Use in alert rule actions
    ```
    
    Slack app must be installed in workspace first via **Sentry Settings → Integrations → Slack**.
    
    ## Orchestration Function
    
    ```typescript
    async function configureSentryAlerts(project: string, org: string = 'emdash'): Promise<void> {
      const baseUrl = 'https://sentry.megabyte.space/api/0';
      const headers = {
        'Authorization': `Bearer ${env.SENTRY_AUTH_TOKEN}`,
        'Content-Type': 'application/json',
      };
    
      // Get Slack integration ID (cached after first call)
      const integrations = await fetch(`${baseUrl}/organizations/${org}/integrations/?provider_key=slack`, { headers });
      const slackIntegration = (await integrations.json())[0];
      const slackId = slackIntegration.id;
      const workspaceId = slackIntegration.externalId;
    
      // Create all 4 rules in parallel
      const issueRulesUrl = `${baseUrl}/projects/${org}/${project}/rules/`;
      const metricRulesUrl = `${baseUrl}/projects/${org}/${project}/alert-rules/`;
    
      await Promise.all([
        fetch(issueRulesUrl, { method: 'POST', headers, body: JSON.stringify(errorSpikeRule(project, workspaceId)) }),
        fetch(issueRulesUrl, { method: 'POST', headers, body: JSON.stringify(newIssueRule(project, workspaceId)) }),
        fetch(issueRulesUrl, { method: 'POST', headers, body: JSON.stringify(unhandledRejectionRule(project, workspaceId)) }),
        fetch(metricRulesUrl, { method: 'POST', headers, body: JSON.stringify(p95LatencyRule(project, slackId)) }),
      ]);
    }
    ```
    
    ## Deploy Silence Windows
    
    Suppress alerts during deploys to avoid false positives from cold starts and transient errors.
    
    ```typescript
    async function silenceDuringDeploy(project: string, durationMinutes: number = 10): Promise<string> {
      const baseUrl = 'https://sentry.megabyte.space/api/0';
      const now = new Date();
      const until = new Date(now.getTime() + durationMinutes * 60000);
    
      // Mute all alert rules for the project during deploy window
      const rules = await fetch(`${baseUrl}/projects/emdash/${project}/rules/`, {
        headers: { 'Authorization': `Bearer ${env.SENTRY_AUTH_TOKEN}` },
      });
      const ruleList = await rules.json();
    
      for (const rule of ruleList) {
        await fetch(`${baseUrl}/projects/emdash/${project}/rules/${rule.id}/snooze/`, {
          method: 'POST',
          headers: { 'Authorization': `Bearer ${env.SENTRY_AUTH_TOKEN}`, 'Content-Type': 'application/json' },
          body: JSON.stringify({ target: 'everyone', until: until.toISOString() }),
        });
      }
    
      return until.toISOString();
    }
    ```
    
    ### Deploy flow integration
    
    1. `silenceDuringDeploy(project, 10)` — mute alerts
    2. `wrangler deploy` — deploy Worker
    3. Purge CDN cache — clear stale responses
    4. E2E tests on prod — verify
    5. Alerts auto-unmute after window — resume monitoring
    
    ## Per-Project Isolation
    
    Every project gets its own alert rule set. Never use organization-wide alert rules — they cause noise from unrelated projects.
    
    **Naming convention** — `[{project}] {Alert Name}` — square brackets make filtering easy in Slack.
    
    When a project is decommissioned, delete its alert rules:
    
    ```bash
    # List and delete all rules for a project
    curl "https://sentry.megabyte.space/api/0/projects/{org}/{project}/rules/" \
      -H "Authorization: Bearer $SENTRY_AUTH_TOKEN" | \
      jq '.[].id' | xargs -I{} curl -X DELETE \
      "https://sentry.megabyte.space/api/0/projects/{org}/{project}/rules/{}/" \
      -H "Authorization: Bearer $SENTRY_AUTH_TOKEN"
    ```
    
    ## Sentry SDK Setup (v10.50+ — Hono + Bun)
    
    ```typescript
    // src/instrument.ts — dedicated @sentry/hono/bun entry point (Apr 2026)
    import * as Sentry from '@sentry/hono/bun';
    
    Sentry.init({
      dsn: 'https://KEY@sentry.megabyte.space/ID',
      environment: 'production',
      tracesSampleRate: 0.1,
      replaysOnErrorSampleRate: 1.0,
      release: env.VERSION,
      integrations: [
        Sentry.viewHierarchyIntegration(), // v10.50: DOM capture on error
      ],
    });
    
    // Apply to Hono app
    import { sentryMiddleware } from '@sentry/hono';
    app.use('*', sentryMiddleware());
    ```
    
    ### DO Alarm Tracing (v10.49)
    
    ```typescript
    // Durable Object alarms auto-traced by Sentry v10.49+
    // No extra config — alarm() calls get spans + error capture
    ```
    
    ### WorkerEntrypoint Support (v10.48)
    
    ```typescript
    // Service bindings auto-instrumented via WorkerEntrypoint class
    // import { WorkerEntrypoint } from 'cloudflare:workers';
    // Sentry traces cross-worker calls automatically
    ```
    
    Minimum version: `@sentry/hono@10.50.0` + `@sentry/bun@10.50.0`. Replaces `toucan-js` for new projects.
    
    ## Integration with 08-deploy Skill
    
    After every first deploy, auto-run `configureSentryAlerts(projectSlug)`. The deploy skill checks if alert rules exist — if not, creates them. Subsequent deploys only trigger silence windows.
    
  • SKILL.md 6.5 KB
    ---
    name: "observability-and-growth"
    description: "Full instrumentation from day one. PostHog consolidates product analytics + feature flags + error tracking (one platform, one bill). GA4 via GTM (14-step automation, custom dimensions over events, server-side tagging). Sentry (deep error tracking + performance). Stripe (webhook-first with idempotent processing). Listmonk on Coolify (newsletters via Amazon SES SMTP relay). PLG 7-layer framework. Programmatic SEO (5 page types). Incident auto-remediation via Sentry→Inngest pipeline. AI search (GEO) awareness. Local business conversions (phone_click, direction_click, form_submit, booking_click) with CRO patterns for both SaaS and local."
    metadata:
      version: "2.1.0"
      updated: "2026-05-03"
      effort: "high"
      model: "sonnet"
    license: "Rutgers"
    compatibility:
      claude-code: ">=2.0.0"
      agentskills: ">=1.0.0"
    submodules:
      - analytics-configuration.md
      - conversion-optimization.md
      - email-marketing-and-listmonk.md
      - feature-flags-and-experiments.md
      - sentry-alert-rules.md
      - square-payments.md
      - stripe-billing.md
      - user-feedback-collection.md
      - workers-tracing-otlp.md
    priority: 2
    pack: "backend"
    stage: stable
    triggers:
      - "posthog"
      - "sentry"
      - "analytics"
      - "stripe"
      - "growth"
    paths:
      - "concern:observability"
    ---
    
    # 13 — Observability and Growth
    
    Tiered PostHog+Sentry+GA4 stacks wired from day one; feature flags, PLG patterns, incident auto-remediation.
    
    ## Instrumentation tiers
    
    Per `_kernel/standards.md#integrations`:
    
    - **Tier 1 (solo)** — PostHog + Workers Tracing OTLP (2 vendors max, cookie-free, free tier covers <10k MAU)
    - **Tier 2 (enterprise)** — + Sentry @sentry/cloudflare v9 + GA4/GTM + Axiom
    - **Tier 3 (LLM-heavy >10k calls/mo)** — + AI Gateway
    
    ## PostHog (Tier 1 cornerstone)
    
    - Snippet on every HTML page w/ `persistence:'memory'` (cookie-free); `capture_pageview` + `capture_pageleave` + `autocapture:true`
    - Unified platform: product analytics + feature flags + session replay + error tracking
    - CSP: `script-src` + `connect-src` for posthog domain
    - Per-feature event naming: `<feature>:<action>` (`signup:complete`, `editor:save`, `share:copy`)
    
    ## Sentry (Tier 2)
    
    - `@sentry/cloudflare` v9 + `withSentry` wrapper; project via `mcp__sentry__create_project` (org:`megabyte-labs`)
    - `SENTRY_DSN` via `wrangler secret put`
    - Pattern: `withSentry(env => ({ dsn, tracesSampleRate: 1.0, sendDefaultPii: false }), worker)`
    - Breadcrumbs before risky ops; capture exception w/ context tags (`worker` | `route` | `userId`)
    - Release tracking via `SENTRY_RELEASE` env; Workers Tracing handles I/O spans
    
    ## Workers Tracing (Tier 1 + 2) / GA4 + GTM (Tier 2) / AI Gateway (Tier 3)
    
    - Workers Tracing: `[observability] enabled = true` in `wrangler.jsonc` — zero-config OTel I/O tracing; export to Axiom, Honeycomb, Grafana, Datadog via `@opentelemetry/exporter-trace-otlp-http`
    - GA4 + GTM: container snippet (head script + noscript iframe); CSP: `googletagmanager.com` + `google-analytics.com` + `analytics.google.com` + `region1.google-analytics.com`; server-side tagging for EU; custom dimensions over custom events
    - AI Gateway: `env.AI.run()` auto-routes; direct Anthropic: `https://gateway.ai.cloudflare.com/v1/{account}/{gateway}/anthropic/v1/messages`; caching + rate-limit + fallback + per-call logging
    
    ## Stripe (SaaS billing only — per `rules/payments-routing.md`)
    
    - Webhook-first w/ idempotent processing (D1 dedupe table `payment_events(event_id, source, processed_at)` UNIQUE)
    - `Stripe-Signature` HMAC + 5-min replay window
    - Mint products + prices via MCP (idempotent via `lookup_key`); subscription state machine in D1
    - `STRIPE_WEBHOOK_SECRET` via `POST /v1/webhook_endpoints`
    
    ## Square (accept-money default — per `rules/payments-routing.md`)
    
    - Square Web Payments SDK card form + Apple Pay + Google Pay + Cash App Pay
    - `Square-Signature` HMAC-SHA256 w/ 6-hr replay window; `idempotency_key` UUID per request (24-hr dedupe)
    - Nonprofit verified-501c3 discount (2.6%+10¢ vs 3.5%+15¢)
    
    ## Listmonk (newsletter — self-hosted on Coolify)
    
    - Amazon SES SMTP relay (`LISTMONK_FROM_EMAIL`); `listmonkSendTx(env, { templateAlias, ... })` via KV-cached alias→id map
    - Templates in `emails/*.html` synced via `scripts/listmonk-sync.mjs`
    - Auth: `Authorization: token <user>:<key>` (Listmonk 3.x API-user pattern)
    
    ## PLG 7-Layer Framework
    
    1. **Discovery** — SEO + AI search + word-of-mouth + paid
    2. **Sign-up** — passwordless preferred (Clerk M2M JWT)
    3. **Activation** — first-value-in-X-min metric (aha moment per `rules/feature-flags.md` instrumentation)
    4. **Engagement** — DAU/MAU ratio, session depth
    5. **Retention** — D1/D7/D30 cohort
    6. **Revenue** — upgrade trigger, expansion
    7. **Referral** — viral coefficient, two-sided rewards
    
    ## Programmatic SEO (5 page types)
    
    - `/integrations/{tool}` · `/compare/{a}-vs-{b}` · `/for/{audience}` · `/templates/{type}` · `/{city}-{service}`
    - Each: unique H1 + meta desc + 800+ unique words + 1 unique image + 3+ internal links + 1+ outbound citation. Cap 200 pages per axis.
    - Per `rules/copy-writing.md` § pSEO + `rules/thin-source-amplification.md`.
    
    ## GEO / AI search
    
    - Quotable answer blocks 40-60 words (LLM citation magnet); FAQPage schema highest AI-citation rate (ChatGPT / Perplexity / Google AI Overviews)
    - JSON-LD facts MUST also appear as visible HTML body text; lead paragraphs answer query in <40 words
    - EEAT: author bio + `Person` schema + `sameAs` + dated revision + ownership statement
    - `llms.txt` at site root (DX-only, <0.3% adoption — not build gate)
    
    ## Local-business conversions + CRO
    
    Track per `local-conversions.md`: `phone_click` (`tel:`) · `direction_click` (Google Maps) · `form_submit` · `booking_click` (Calendly/Cal.com) · `chat_click` · `review_click` (Google Business/Yelp). Each fires PostHog + Sentry breadcrumb + (Tier 2) GA4 conversion event.
    
    CRO: sticky CTA bar on mobile · scroll-progress bar (subtle) · exit-intent modal (cart/pricing only, not blogs) · social proof near every CTA · urgency without dark patterns ("3 spots left this week" if true) · trust strip above fold · single primary CTA per surface.
    
    ## Incident auto-remediation
    
    Sentry → Inngest pipeline: (1) Sentry webhook on `event.alert.triggered` → (2) Inngest dispatches `incident-responder` agent → (3) agent reads event, traces to source file, proposes fix, opens PR via gh MCP → (4) PR auto-merges if all CI gates pass per `rules/ai-seniority.md`.
    
    ## See submodules: posthog, ga4-gtm, sentry, stripe-billing, square-payments, listmonk, plg-framework, programmatic-seo, incident-remediation, geo-ai-search, local-conversions.
    
  • square-payments.md 13.2 KB
    ---
    name: "Square Payments"
    description: "Default payment processor for nonprofits, donations, one-time charges, in-person POS, and small-business e-commerce. Square Web Payments SDK (card + Apple Pay + Cash App Pay), Payments API for server-side charges, idempotency-keyed REST. Cheaper effective rates than Stripe for sub-$100 donations and nonprofit-discount accounts. Use Stripe instead when SaaS subscriptions / metered usage / Entitlements / Connect marketplaces are involved."
    updated: "2026-05-10"
    ---
    
    # Square Payments
    
    ## When Square is the Default (***FIRST DECISION — EVERY PAYMENT FEATURE***)
    
    ### Square defaults
    
    - Nonprofits + donations (one-time OR recurring via Square Subscriptions)
    - Small-business e-commerce
    - Restaurant / retail / salon / medical with in-person POS need
    - Event ticketing
    - Sub-$100 average ticket where Stripe's $0.30 fixed is punishing
    - Any client already on Square POS (single source of truth for in-person + online)
    - NJ/NY local-business rebuilds where Square Retail penetration is high
    
    ### Stripe overrides
    
    - SaaS subscriptions with seat-based / tiered / usage-based billing
    - Metered billing (Stripe Billing Meters API)
    - Entitlements feature gating
    - Stripe Connect (marketplaces, split payments)
    - Stripe Tax (40+ country auto-collection)
    - Stripe Atlas formation flows
    - Embedded checkout for agentic commerce (ACP launch partners)
    - Enterprise contracts requiring net-30 invoicing
    
    If ANY of these apply → Stripe; else → Square.
    
    Hybrid is allowed: Square for donations + Stripe for SaaS subscription on the same site. Each integration owns its own webhook + DB tables — never cross-pollinate idempotency keys.
    
    ## Default Pricing Models
    
    ### Nonprofit / Donation (Square default)
    
    - **$10** — "Feeds a family for a day"
    - **$25** — "Covers supplies for a week"
    - **$50** — "Powers the hotline for a month"
    - **$100** — "Sponsors a full program"
    - **$250** — "Transforms a life"
    - **$500** — "Makes a lasting impact"
    - **Custom** — "Choose your amount"
    - One-time as default (Square Subscriptions adds setup complexity)
    - Monthly recurring opt-in via Square Subscriptions API + Plans — only when client has staff to manage
    - Impact labels mandatory — specific + concrete, never vague
    - Progress bar showing goal achievement (optional, requires Durable Object aggregation)
    
    ### Square Nonprofit Discount
    
    501(c)(3) orgs qualify for **2.2% + $0.30** vs standard **2.6% + $0.10**. Apply at `squareup.com/help/us/en/article/6358` with EIN + IRS determination letter. For $25 average donation: nonprofit fee = $0.85 vs Stripe $1.03 (~10% per-transaction savings).
    
    ## Web Payments SDK (Browser — Tokenize Card Client-Side)
    
    ```html
    <!-- production -->
    <script src="https://web.squarecdn.com/v1/square.js"></script>
    <!-- sandbox -->
    <script src="https://sandbox.web.squarecdn.com/v1/square.js"></script>
    ```
    
    ```typescript
    const payments = window.Square.payments(applicationId, locationId);
    const card = await payments.card();
    await card.attach('#square-card-container');
    // On submit:
    const { token, status } = await card.tokenize();
    if (status !== 'OK') throw new Error('Tokenization failed');
    // POST token + amount to your worker — NEVER charge from the browser
    await fetch('/api/donate', { method: 'POST', body: JSON.stringify({ sourceId: token, amountCents: 5000 }) });
    ```
    
    Optional payment methods:
    
    - `payments.applePay(paymentRequest)`
    - `payments.cashAppPay(paymentRequest, { redirectURL })`
    - `payments.googlePay(paymentRequest)`
    - `payments.ach({ accountHolderName, plaidLinkToken })`
    
    ## Server-Side Charge (Cloudflare Worker — vanilla fetch)
    
    ```typescript
    const SQUARE_API_VERSION = '2024-12-18';
    function squareApiBase(env: Env) {
      return env.SQUARE_ENVIRONMENT === 'production'
        ? 'https://connect.squareup.com'
        : 'https://connect.squareupsandbox.com';
    }
    
    async function chargeOnce(env: Env, sourceId: string, amountCents: number, email: string, note: string) {
      const res = await fetch(`${squareApiBase(env)}/v2/payments`, {
        method: 'POST',
        headers: {
          'Square-Version': SQUARE_API_VERSION,
          'Authorization': `Bearer ${env.SQUARE_ACCESS_TOKEN}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          source_id: sourceId,
          idempotency_key: crypto.randomUUID(),
          amount_money: { amount: amountCents, currency: 'USD' },
          location_id: env.SQUARE_LOCATION_ID,
          autocomplete: true,
          buyer_email_address: email,
          note,
          reference_id: `donation-${Date.now()}`,
        }),
      });
      const body = await res.json();
      if (!res.ok) throw new Error(body.errors?.[0]?.detail ?? 'Square charge failed');
      return { paymentId: body.payment.id, receiptUrl: body.payment.receipt_url };
    }
    ```
    
    Every charge MUST include a unique `idempotency_key` (UUID) — retrying with the same key returns the original payment, never double-charges.
    
    ## Webhook Handler (Cloudflare Worker)
    
    ```typescript
    // Verify signature via X-Square-HmacSha256-Signature header
    async function verifySquareSignature(req: Request, env: Env): Promise<boolean> {
      const sig = req.headers.get('x-square-hmacsha256-signature');
      if (!sig) return false;
      const body = await req.text();
      const url = new URL(req.url).toString();
      const key = await crypto.subtle.importKey(
        'raw',
        new TextEncoder().encode(env.SQUARE_WEBHOOK_SIGNATURE_KEY),
        { name: 'HMAC', hash: 'SHA-256' },
        false,
        ['sign']
      );
      const computed = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(url + body));
      const computedB64 = btoa(String.fromCharCode(...new Uint8Array(computed)));
      return computedB64 === sig;
    }
    
    app.post('/api/webhooks/square', async (req, env) => {
      if (!(await verifySquareSignature(req, env))) return new Response('Forbidden', { status: 403 });
      const event = await req.json();
      switch (event.type) {
        case 'payment.created':       /* record + send receipt via Resend */ break;
        case 'payment.updated':       /* status changed APPROVED → COMPLETED */ break;
        case 'refund.created':        /* notify donor + update DB */ break;
        case 'subscription.created':  /* recurring donor activated */ break;
        case 'subscription.canceled': /* retention email */ break;
      }
      return new Response('OK');
    });
    ```
    
    ## Subscriptions (Recurring Donations)
    
    Square Subscriptions requires a Plan + Plan Variation first. Two-step setup:
    
    ```typescript
    // 1. Create catalog plan (once, via wrangler or admin tool)
    await fetch(`${apiBase}/v2/catalog/object`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        idempotency_key: crypto.randomUUID(),
        object: {
          type: 'SUBSCRIPTION_PLAN',
          id: '#monthly-25',
          subscription_plan_data: { name: 'Monthly $25 Donor' },
        },
      }),
    });
    // 2. Create subscription (per donor)
    await fetch(`${apiBase}/v2/subscriptions`, {
      method: 'POST',
      headers,
      body: JSON.stringify({
        idempotency_key: crypto.randomUUID(),
        location_id: env.SQUARE_LOCATION_ID,
        plan_variation_id: 'plan_variation_id_from_step_1',
        customer_id,        // create via /v2/customers first
        card_id,            // saved card token from cards.create
      }),
    });
    ```
    
    For small nonprofits without staff to manage Plans, default to one-time charges + "donate again" CTA in receipt email.
    
    ## Environment Toggle
    
    - `SQUARE_ACCESS_TOKEN` — sandbox or production
    - `SQUARE_LOCATION_ID` — receiving location
    - `SQUARE_APPLICATION_ID` — Web Payments SDK app ID
    - `SQUARE_ENVIRONMENT` — `"sandbox"` (default) or `"production"`
    - `SQUARE_WEBHOOK_SIGNATURE_KEY` — for webhook HMAC verification
    
    Sandbox creds: `developer.squareup.com → Sandbox`; production: `developer.squareup.com → Production` (requires verified business). Test card: `4111 1111 1111 1111` / any future date / any CVV / any ZIP.
    
    ## CSP Allowances (every site using Square Web Payments SDK)
    
    ```
    script-src 'self' https://web.squarecdn.com https://sandbox.web.squarecdn.com 'unsafe-inline'
    style-src 'self' https://web.squarecdn.com https://sandbox.web.squarecdn.com 'unsafe-inline'
    frame-src https://*.squarecdn.com https://*.squareup.com
    connect-src 'self' https://*.squarecdn.com https://*.squareup.com https://*.squareupsandbox.com https://pci-connect.squareup.com https://pci-connect.squareupsandbox.com
    img-src 'self' https://*.squarecdn.com https://*.squareup.com data:
    ```
    
    ## Donation Page Design (givedirectly.org energy)
    
    - Hero with cause story + impact stat counter (rAF roll-in, IntersectionObserver-gated)
    - Amount selector grid (6 presets + custom input)
    - Frequency toggle (one-time | monthly) — default one-time unless client has subscription infrastructure
    - Donor form (name, email, optional address for tax receipt)
    - Tribute-gift section (optional collapsed accordion: "in honor of" / "in memory of")
    - Square card container `<div id="square-card-container">`
    - Apple Pay / Cash App Pay buttons above card form (when enabled)
    - Trust badge: "Square Secured · PCI-DSS Level 1 · 256-bit TLS"
    - Turnstile invisible widget
    
    ### Post-Donation Flow
    
    1. Success page with confetti (06/easter-eggs) + animated impact statement
    2. Receipt email via Resend: thank you + tax-deductible language + Square receipt URL + "donate again" CTA
    3. PostHog event: `donation_complete` with `{ amountCents, frequency, paymentMethod }`
    4. Sentry breadcrumb: `donation.completed` with `paymentId` tag
    5. GA4 event: `purchase` with full e-commerce schema
    
    ## Auto-Detect Square Account
    
    - `GET /v2/locations`: single location → auto-use as `SQUARE_LOCATION_ID`; multi-location → ask which is receiving; no account → prompt signup at `squareup.com/signup`
    
    ## Square API Coverage
    
    - **Payments** — `/v2/payments` — one-time charges
    - **Customers** — `/v2/customers` — donor records, repeat-donor lookup
    - **Cards (on file)** — `/v2/cards` — save card for recurring without storing CC
    - **Subscriptions** — `/v2/subscriptions` — recurring donations (requires Plans)
    - **Catalog** — `/v2/catalog/object` — Plan + Plan Variation setup
    - **Refunds** — `/v2/refunds` — donor-requested refund within 60 days
    - **Disputes** — `/v2/disputes` — chargeback handling
    - **Locations** — `/v2/locations` — multi-location accounts
    - **Orders** — `/v2/orders` — e-commerce line items (when not donation)
    - **Inventory** — `/v2/inventory` — retail-tier sites only
    - **Loyalty** — `/v2/loyalty` — repeat-donor rewards (rare for nonprofits)
    - **Gift Cards** — `/v2/gift-cards` — holiday gift-of-meals campaigns
    
    ## Square vs Stripe Decision Cheat Sheet
    
    - **Nonprofit donation page** → **Square** (nonprofit-rate discount, simpler UX)
    - **Small biz one-time checkout** → **Square** (cheaper for sub-$100 tickets)
    - **Restaurant / salon / retail with POS** → **Square** (single source of truth for in-person + online)
    - **SaaS subscriptions with seats/tiers** → **Stripe** (Billing API, Entitlements, prorations)
    - **Metered/usage-based AI billing** → **Stripe** (Billing Meters API, Metronome)
    - **Marketplace with split payments** → **Stripe Connect**
    - **Embedded checkout for AI agents** → **Stripe** (ACP launch partner with OpenAI/Perplexity)
    - **Multi-country tax automation** → **Stripe Tax**
    - **Net-30 enterprise invoicing** → **Stripe** (Invoicing API)
    - **Donor wants to use Cash App** → **Square** (native Cash App Pay)
    - **Recurring donations, no subscription complexity** → **Square** one-time + "donate again" email
    - **Recurring donations, full retention machine** → either — Stripe Customer Portal is more polished, Square Subscriptions is cheaper
    
    ## Conversion Optimization (Research-Backed)
    
    - Donation page conversion median: 17% (NextAfter 2024); top 10%: 30%+
    - Suggested amount with social proof lifts AOV 12%: `"$25 — most common gift this month"`
    - One-time default for new donors converts higher than monthly (lowers commitment friction)
    - Cash App Pay adds 8-15% to total donations on Gen Z / Millennial audiences
    - Apple Pay reduces mobile abandonment 30-40% (Source: Apple Pay merchant data)
    - Inline card form (Square Web Payments SDK) converts higher than redirect (Square Checkout)
    - Show `"Powered by Square"` security badge; progress indicator for multi-step forms
    - Guest checkout default; show total prominently before final submit
    
    ## Receipt Email Template (Resend)
    
    ```
    Subject: Thank you, {{firstName}} — your donation to {{orgName}} confirmed
    
    Hi {{firstName}},
    
    We received your gift of {{amountFormatted}} on {{date}}.
    
    {{#if tribute}}
    This gift was made in {{tributeType}} of {{tributeName}}.
    {{/if}}
    
    Your contribution is tax-deductible. {{orgName}} is a 501(c)(3) nonprofit organization, EIN {{ein}}.
    
    Official Square receipt: {{squareReceiptUrl}}
    Print-friendly version: {{printableReceiptUrl}}
    
    {{#if recurring}}
    Manage your recurring donation: {{donorPortalUrl}}
    {{else}}
    Make another gift: {{donatePageUrl}}
    {{/if}}
    
    With gratitude,
    {{orgName}} Team
    {{orgAddress}}
    ```
    
    ## Ownership
    
    - **Owns** — Square Web Payments SDK integration, Payments API (server-side charge), Subscriptions for recurring donations, webhook signature verification, idempotency-key patterns, nonprofit-rate enrollment, Square POS sync for hybrid online + in-person clients, Cash App Pay + Apple Pay enablement
    - **Never owns** — SaaS subscriptions / metered billing / Entitlements (→ 13/stripe-billing), email send (→ 09/email-templates), donor analytics (→ 13/analytics-configuration), goal-progress aggregation (→ 05/heartbeat-polling + Durable Objects)
    
  • stripe-billing.md 9.9 KB
    ---
    name: "Stripe Billing"
    description: "SaaS subscriptions + enterprise billing default. Tiered pricing, seat-based billing, Stripe Tax, Billing Meters (metered usage), Entitlements (feature gating), Connect (marketplaces), Atlas, Agentic Commerce (ACP). Use Stripe ONLY when SaaS subscriptions / metered usage / Entitlements / Connect marketplaces / enterprise invoicing are involved — for nonprofits, donations, and small-business one-time charges use 13/square-payments instead."
    updated: "2026-05-10"
    ---
    
    > **Routing rule (***FIRST DECISION***):** If the feature is nonprofit donations, small-business one-time checkout, restaurant/retail POS, or sub-$100 average ticket → use [13/square-payments](square-payments.md). Stripe is reserved for SaaS subscriptions, metered usage, Entitlements feature gating, Connect marketplaces, and enterprise invoicing. Hybrid is allowed (Square for donations + Stripe for SaaS subs on same site).
    
    # Stripe Billing
    
    ## Default Pricing Models
    
    ### SaaS Products
    
    - **Free** — $0/mo — limited features, no payment required
    - **Pro** — $50/mo — all features, priority support, cancel anytime
    - **Enterprise** — custom — SSO, SLA, dedicated support, volume pricing
    
    **Seat-Based Billing** — `quantity` on subscription items:
    
    ```typescript
    stripe.subscriptions.update(subId, { items: [{ id: itemId, quantity: seatCount }] })
    ```
    
    - Auto-adjust on member add/remove via Clerk org webhook; prorate mid-cycle
    - Display: `"$12/seat/mo × 8 seats = $96/mo"`
    
    **Stripe Tax** — automatic tax calculation:
    
    ```typescript
    stripe.subscriptions.create({ automatic_tax: { enabled: true } })
    ```
    
    - Requires `tax_behavior: 'exclusive'|'inclusive'` on each Price
    - Register jurisdictions: `stripe.tax.registrations.create({ country, state, type })`
    - Display tax line on invoices; no tax on $0 free tiers
    
    - Annual billing default (20% discount) — $480/year vs $600
    - Stripe Link enabled for one-click checkout (7%+ conversion lift)
    - Show monthly price on annual plan: `"$40/mo billed annually"`
    
    ### Nonprofit / Donation
    
    - **$10** — "Feeds a family for a day"
    - **$25** — "Covers supplies for a week"
    - **$50** — "Powers the hotline for a month"
    - **$100** — "Sponsors a full program"
    - **$250** — "Transforms a life"
    - **$500** — "Makes a lasting impact"
    - **Custom** — "Choose your amount"
    - Monthly recurring as default; one-time as option
    - Impact labels on every amount — specific, concrete, not vague
    - Progress bar showing goal achievement
    
    ## Auto-Setup Workflow
    
    ### 1. Create Stripe Product + Prices
    
    ```typescript
    const stripe = new Stripe(env.STRIPE_SECRET_KEY);
    
    // Create product
    const product = await stripe.products.create({
      name: 'Project Name Pro',
      description: 'All features, priority support',
      metadata: { project: 'domain.com' },
    });
    
    // Create prices
    const monthlyPrice = await stripe.prices.create({
      product: product.id,
      unit_amount: 5000, // $50.00
      currency: 'usd',
      recurring: { interval: 'month' },
    });
    
    const annualPrice = await stripe.prices.create({
      product: product.id,
      unit_amount: 48000, // $480.00 (20% off)
      currency: 'usd',
      recurring: { interval: 'year' },
    });
    ```
    
    ### 2. Create Checkout Endpoint
    
    ```typescript
    app.post('/api/checkout', async (c) => {
      const { priceId, successUrl, cancelUrl } = await c.req.json();
    
      const session = await stripe.checkout.sessions.create({
        mode: 'subscription', // or 'payment' for one-time
        payment_method_types: ['card', 'link'], // Stripe Link for one-click
        line_items: [{ price: priceId, quantity: 1 }],
        success_url: successUrl || `${c.req.url.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
        cancel_url: cancelUrl || `${c.req.url.origin}/pricing`,
        allow_promotion_codes: true,
        billing_address_collection: 'auto',
        customer_creation: 'always',
      });
    
      return c.json({ url: session.url });
    });
    ```
    
    ### 3. Brand the Checkout
    
    ```typescript
    // Set Stripe account branding to match site
    await stripe.accounts.update('acct_...', {
      settings: {
        branding: {
          icon: 'https://domain.com/favicon-32x32.png',
          logo: 'https://domain.com/logo.png',
          primary_color: '#00E5FF',
          secondary_color: '#060610',
        }
      }
    });
    ```
    
    ### 4. Webhook Handler
    
    ```typescript
    app.post('/api/webhooks/stripe', async (c) => {
      const sig = c.req.header('stripe-signature')!;
      const body = await c.req.text();
      const event = stripe.webhooks.constructEvent(body, sig, env.STRIPE_WEBHOOK_SECRET);
    
      switch (event.type) {
        case 'checkout.session.completed':
          // Activate subscription, send welcome email via Resend
          break;
        case 'customer.subscription.deleted':
          // Deactivate subscription, send retention email
          break;
        case 'invoice.payment_failed':
          // Notify user, retry logic
          break;
      }
      return c.json({ received: true });
    });
    ```
    
    ## Donation Page Design (givedirectly.org style)
    
    - Full-screen split: left 60% cause imagery + impact story + real numbers; right 40% amount selector + checkout form + progress bar
    
    ### Donation Goal + Progress Bar
    
    ```typescript
    // Real-time via Durable Objects + Stripe webhooks
    app.get('/api/donation-progress', async (c) => {
      const goal = 10000; // $10,000 goal
      const raised = await getDonationTotal(c.env.DO); // from Durable Object
      return c.json({ goal, raised, percentage: Math.min(100, (raised / goal) * 100) });
    });
    ```
    
    ### Post-Donation Flow
    
    1. Success page with confetti animation (06/easter-eggs energy)
    2. Auto-email via Resend: thank you + tax receipt + ask to share
    3. Auto-email all participants when goal is met
    4. PostHog event: `donation_complete` with amount and method
    
    ## Conversion Optimization (Research-Backed)
    
    - 3 tiers maximum (paradox of choice — 3 converts better than 5)
    - Highlight middle tier as "Most Popular" (anchoring effect)
    - Show annual savings prominently (`"Save $120/year"`)
    - Include FAQ below pricing; feature comparison table
    - Trust signals near CTA: `"Trusted by X users"`, security badge
    - Stripe Link reduces checkout time 7× (one-click for returning users)
    - Show `"Powered by Stripe"` security badge near payment form
    - Guest checkout default; show total prominently before final submit
    
    ## Psychology of Pricing (04/wisdom-and-human-psychology)
    
    - **Anchoring (Kahneman)** — show annual price first; $480/year makes $50/month feel reasonable
    - **Loss Aversion (Kahneman & Tversky)** — `"Don't lose your progress"` > `"Keep your progress"`
    - **Reciprocity (Cialdini)** — generous free tier creates internal drive to convert or refer
    - **Social Proof (Cialdini)** — `"Join 1,200 supporters"` near every pricing CTA
    - **Peak-End Rule (Kahneman)** — confetti + warm email + later impact update = memory of joy
    
    ### The Ethical Line
    
    - Real scarcity only (actual limited spots, actual deadline)
    - No confirmshaming (`"No, I don't want to help"` = dark pattern)
    - Easy cancellation — as easy as signup; no hidden fees at checkout
    
    ## Billing Meter API v2 (GA 2026)
    
    Required for metered/usage-based pricing. `POST /v1/billing/meters` creates meter; `POST /v1/billing/meter_events` streams events. Track usage in KV → batch flush via `ctx.waitUntil()`.
    
    ```typescript
    // Create meter (once)
    const meter = await stripe.billing.meters.create({
      display_name: 'API Calls',
      event_name: 'api_call',
      default_aggregation: { formula: 'sum' },
    });
    // Stream events (per request)
    await stripe.billing.meterEvents.create({
      event_name: 'api_call',
      payload: { stripe_customer_id: customerId, value: '1' },
    });
    ```
    
    ## Entitlements API (GA 2026)
    
    Define Features (`POST /v1/entitlements/features`), attach to Products; check at API boundary:
    
    ```typescript
    const { data } = await stripe.entitlements.activeEntitlements.list({ customer: customerId });
    const hasFeature = data.some(e => e.feature.lookup_key === 'advanced_analytics');
    if (!hasFeature) return c.json({ error: 'Upgrade required', code: 'ENTITLEMENT_MISSING' }, 403);
    ```
    
    - Use for plan-tier feature gating; auto-updates when plan changes — no webhook needed for feature access
    
    ## Stripe Agentic Commerce (ACP — 2026)
    
    Agent calls `stripe.checkout.sessions.create()` with `ui_mode: 'embedded'` → user confirms in Stripe-hosted UI → agent receives confirmation. No agent handles raw card data.
    
    ```typescript
    // Agent-initiated checkout (user confirms in Stripe UI)
    const session = await stripe.checkout.sessions.create({
      mode: 'payment',
      ui_mode: 'embedded',
      line_items: [{ price: agentSelectedPriceId, quantity: 1 }],
      return_url: `${origin}/agent-purchase-complete?session_id={CHECKOUT_SESSION_ID}`,
    });
    // Return session.client_secret to agent for user confirmation
    ```
    
    ## Metronome (Acquired Jan 2026)
    
    Multidimensional metering for AI/usage-based billing at scale: per-token, per-seat, per-API-call combos. Supersedes Billing Meter API for complex scenarios.
    
    ## Key Locations
    
    - **Stripe API key** — shared key pool (05/shared-api-pool)
    - **Webhook secret** — `wrangler secret` per project
    - **Test mode** — always use `sk_test_` before going live
    
    ## SaaS Business Strategy (Source: Stripe Atlas Guides + YC 2026)
    
    ### Two Core SaaS Models
    
    - **Low-Touch SaaS** — self-serve; optimize for onboarding speed + activation rate; target monthly churn <5% (ideally <3%); growth via PLG + SEO
    - **High-Touch SaaS** — enterprise sales; optimize for demo + integration support; target Net Revenue Retention >120%; growth via outbound + partnerships
    
    ### Critical SaaS Metrics (Monitor From Day 1)
    
    - **MRR** — growing month-over-month (primary health indicator)
    - **Churn Rate** — <5% monthly SMB, <1% enterprise
    - **LTV:CAC Ratio** — >3:1
    - **Time to Value** — <60 seconds for free tier
    - **Activation Rate** — 20-40% of signups
    
    ### Pricing Strategy (YC + Stripe Consensus)
    
    1. Generous free tier → higher paid conversion via reciprocity
    2. Annual billing default + 20% discount → lower churn, better cash flow
    3. Price anchoring — show enterprise/annual first
    4. Three tiers maximum (Hick's Law)
    5. AI-native baseline — every 2026 SaaS delivers more value than static software
    
  • user-feedback-collection.md 8.9 KB
    ---
    name: "User Feedback Collection"
    description: "In-app feedback widget: 5-star rating + text comment, stored in D1, reviewed at /admin/feedback. NPS survey at day 7 and day 30 via PostHog. Testimonial collection at /feedback for moderated display. Tracks satisfaction trends over time."
    updated: "2026-04-23"
    ---
    
    # User Feedback Collection
    
    ## Feedback Widget (In-App)
    
    ### HTML
    
    ```html
    <button id="feedbackBtn" class="feedback-trigger" aria-label="Give feedback">
      💬
    </button>
    <div id="feedbackModal" class="feedback-modal" hidden>
      <form id="feedbackForm">
        <h3>How's your experience?</h3>
        <div class="rating" role="radiogroup" aria-label="Rating">
          ${[1,2,3,4,5].map(n => `
            <button type="button" class="star" data-value="${n}" aria-label="${n} stars">
              ${'★'.repeat(n)}${'☆'.repeat(5-n)}
            </button>
          `).join('')}
        </div>
        <textarea name="comment" placeholder="Tell us more (optional)" rows="3" maxlength="1000"></textarea>
        <button type="submit">Send Feedback</button>
      </form>
    </div>
    ```
    
    ### Backend
    
    ```typescript
    const FeedbackSchema = z.object({
      rating: z.number().min(1).max(5),
      comment: z.string().max(1000).optional(),
      page: z.string().optional(),
    });
    
    app.post('/api/feedback', async (c) => {
      const body = await c.req.json();
      const parsed = FeedbackSchema.parse(body);
    
      await c.env.DB.prepare(
        'INSERT INTO feedback (id, rating, comment, page, created_at) VALUES (?, ?, ?, ?, ?)'
      ).bind(ulid(), parsed.rating, parsed.comment || null, parsed.page || c.req.header('referer'), new Date().toISOString()).run();
    
      posthog.capture('feedback_submitted', { rating: parsed.rating });
      return c.json({ success: true });
    });
    ```
    
    ### D1 Schema
    
    ```sql
    CREATE TABLE feedback (
      id TEXT PRIMARY KEY,
      rating INTEGER NOT NULL CHECK(rating BETWEEN 1 AND 5),
      comment TEXT,
      page TEXT,
      status TEXT DEFAULT 'pending', -- pending, approved, rejected
      created_at TEXT NOT NULL
    );
    ```
    
    ## NPS Survey (PostHog)
    
    ### Day 7 + Day 30 Surveys
    
    ```javascript
    // Trigger via PostHog surveys feature
    posthog.init('PROJECT_KEY', {
      api_host: 'https://posthog.megabyte.space',
      surveys: true, // Enable surveys
    });
    ```
    
    Set up in PostHog dashboard:
    
    - **Day 7 survey** — `"How likely are you to recommend [Product]? (0-10)"`
    - **Day 30 survey** — Same question + `"What could we improve?"`
    - Target: users who signed up 7/30 days ago
    
    ### NPS Scoring
    
    - **9-10** — Promoter
    - **7-8** — Passive
    - **0-6** — Detractor
    - **NPS** — `% Promoters - % Detractors` (target: >30)
    
    ## Testimonial Collection (`/feedback`)
    
    Public-facing page for collecting testimonials:
    
    ```typescript
    app.get('/feedback', (c) => c.html(renderFeedbackPage()));
    
    app.post('/api/testimonial', async (c) => {
      const body = await c.req.json();
      const parsed = z.object({
        name: z.string().min(1).max(100),
        role: z.string().max(100).optional(),
        quote: z.string().min(10).max(500),
        rating: z.number().min(1).max(5),
      }).parse(body);
    
      await c.env.DB.prepare(
        'INSERT INTO testimonials (id, name, role, quote, rating, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)'
      ).bind(ulid(), parsed.name, parsed.role, parsed.quote, parsed.rating, 'pending', new Date().toISOString()).run();
    
      return c.json({ success: true, message: 'Thanks! Your testimonial will be reviewed.' });
    });
    ```
    
    **Moderation** — testimonials are `pending` until manually approved. Display approved ones on the homepage as social proof (skill 09).
    
    ## Admin View (`/admin/feedback`)
    
    Simple table showing all feedback with approve/reject buttons. Protected by auth or basic admin password.
    
    ## MCP Tools Available
    
    ### Notion MCP (`mcp__notion__*`) — for feedback storage and triage
    
    - `mcp__notion__API-post-page` — create a feedback entry in Notion database
    - `mcp__notion__API-query-data-source` — query feedback database with filters (rating, status, date)
    - `mcp__notion__API-patch-page` — update feedback status (pending → reviewed → actioned)
    - `mcp__notion__API-post-search` — search feedback by keyword
    - `mcp__notion__API-retrieve-a-database` — get feedback database schema
    - `mcp__notion__API-create-a-comment` — add internal notes to feedback entries
    
    ### Notion Feedback Database Schema
    
    ```
    Database: "User Feedback"
    Properties:
      - Rating (Number, 1-5)
      - Comment (Rich Text)
      - Page URL (URL)
      - Status (Select: Pending, Reviewed, Actioned, Dismissed)
      - Source (Select: Widget, NPS, Testimonial, Email)
      - User Email (Email, optional)
      - Created (Created Time)
      - Priority (Select: Low, Medium, High, Critical)
    ```
    
    ### PostHog MCP (`mcp__posthog__*`) — for feedback analytics
    
    - `mcp__posthog__authenticate` — connect to PostHog instance
    
    PostHog integration points:
    
    - Track `feedback_submitted` event with rating + page properties
    - Track `nps_response` event with score + segment (promoter/passive/detractor)
    - Track `testimonial_submitted` event
    - Create PostHog insight: "Average Rating Over Time" (line chart, weekly)
    - Create PostHog insight: "NPS Score Trend" (line chart, monthly)
    - Feature flag: `show_feedback_widget` — control which pages show the widget
    
    ### Playwright MCP (`mcp__playwright__*`) — for widget testing
    
    - `mcp__playwright__browser_navigate` — navigate to page with feedback widget
    - `mcp__playwright__browser_click` — open feedback modal, click stars, submit
    - `mcp__playwright__browser_take_screenshot` — screenshot widget at each state (closed, open, submitted)
    - `mcp__playwright__browser_snapshot` — verify ARIA roles on rating component
    
    ## Feedback Widget Implementation Details
    
    ### Angular Component (Standalone)
    
    ```typescript
    @Component({
      selector: 'app-feedback-widget',
      standalone: true,
      imports: [CommonModule, ReactiveFormsModule],
      template: `
        <button (click)="isOpen.set(true)" class="feedback-trigger"
                aria-label="Give feedback" data-testid="feedback-trigger">
          <span aria-hidden="true">💬</span>
        </button>
        @if (isOpen()) {
          <div class="feedback-modal" role="dialog" aria-label="Feedback form"
               data-testid="feedback-modal">
            <form [formGroup]="form" (ngSubmit)="submit()">
              <h3>How's your experience?</h3>
              <div role="radiogroup" aria-label="Rating">
                @for (n of [1,2,3,4,5]; track n) {
                  <button type="button" [attr.aria-label]="n + ' stars'"
                          [class.active]="form.get('rating')?.value === n"
                          (click)="form.patchValue({ rating: n })"
                          data-testid="star-{{n}}">
                    {{ n <= (form.get('rating')?.value || 0) ? '★' : '☆' }}
                  </button>
                }
              </div>
              <textarea formControlName="comment" placeholder="Tell us more (optional)"
                        rows="3" maxlength="1000" data-testid="feedback-comment"></textarea>
              <button type="submit" [disabled]="!form.get('rating')?.value"
                      data-testid="feedback-submit">Send Feedback</button>
            </form>
          </div>
        }
      `
    })
    export class FeedbackWidgetComponent {
      isOpen = signal(false);
      form = new FormGroup({
        rating: new FormControl<number | null>(null),
        comment: new FormControl(''),
      });
    
      private http = inject(HttpClient);
    
      submit() {
        this.http.post('/api/feedback', {
          rating: this.form.value.rating,
          comment: this.form.value.comment,
          page: window.location.pathname,
        }).subscribe(() => {
          this.isOpen.set(false);
          this.form.reset();
        });
      }
    }
    ```
    
    ## Computer Use Integration
    
    Use `mcp__computer-use__*` for feedback triage workflows:
    
    1. **Notion dashboard review** — screenshot the Notion feedback database view to see pending feedback at a glance
    2. **PostHog dashboards** — screenshot the NPS trend and rating distribution insights for weekly review
    3. **Widget visual testing** — screenshot the feedback widget at 375px and 1280px to verify it doesn't obstruct content
    
    ## Acceptance Criteria
    
    1. **Feedback widget visible on all pages** — Playwright: `data-testid="feedback-trigger"` visible on homepage + 2 inner pages
    2. **Rating submission works** — POST `/api/feedback` with rating 1-5 returns `{ success: true }`
    3. **Invalid ratings rejected** — POST with rating 0 or 6 returns 400 error
    4. **Comment length enforced** — POST with >1000 char comment returns 400
    5. **Feedback stored in D1** — query D1 `feedback` table; new row exists with correct rating + page
    6. **Feedback synced to Notion** — `mcp__notion__API-query-data-source` returns matching entry
    7. **PostHog event fires** — check PostHog for `feedback_submitted` event with correct properties
    8. **NPS survey triggers at day 7** — user created 7 days ago sees NPS survey (PostHog survey targeting)
    9. **Testimonials require moderation** — new testimonial has `status: 'pending'`, not shown on site until approved
    10. **Widget accessible** — axe-core: 0 violations on feedback modal; keyboard navigable; ARIA roles correct
    11. **Admin view shows all feedback** — `/admin/feedback` renders table with rating, comment, page, status, date
    
  • workers-tracing-otlp.md 3 KB
    # Workers Tracing + OTLP Export — Observability 2026
    
    Cloudflare Workers automatic tracing is in open beta. Zero-config OpenTelemetry instrumentation for every I/O: KV, R2, D1, DO, fetch, AI binding, Vectorize, Queue.
    
    ## Enable
    
    `wrangler.jsonc`:
    
    ```jsonc
    {
      "observability": { "enabled": true }
    }
    ```
    
    Free until **March 1, 2026** — then billed.
    
    ## Export to External Backends
    
    ### Axiom (Cloudflare partner — cheapest at edge volumes)
    
    ```ts
    import { setupAxiomOtel } from '@axiomhq/cloudflare-workers';
    
    setupAxiomOtel({
      dataset: env.AXIOM_DATASET,
      token: env.AXIOM_TOKEN,
    });
    ```
    
    ### Honeycomb (best for trace exploration + BubbleUp anomaly detection)
    
    ```ts
    import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
    
    const exporter = new OTLPTraceExporter({
      url: 'https://api.honeycomb.io/v1/traces',
      headers: { 'x-honeycomb-team': env.HONEYCOMB_API_KEY },
    });
    ```
    
    ### Grafana / Datadog
    
    Same OTLP pattern — change URL + auth headers.
    
    ## Pillar Roles (avoid duplication)
    
    - **Sentry** (`@sentry/cloudflare` v9) — **exceptions** only. Use `withSentry` wrapper. Drop the manual breadcrumb scaffolding for I/O; Workers Tracing covers that.
    - **Workers Tracing (OTLP)** — **spans** for every I/O, perf timing, "where did time go" debugging.
    - **PostHog** — product analytics + session replay + feature flags + frontend error tracking (one platform). New projects can go PostHog-only for frontend, Sentry-only for backend.
    - **GA4 / GTM** — marketing + acquisition + conversion funnels.
    - **AI Gateway** — every LLM call. Logging + caching + rate-limit + fallback in one binding.
    
    ## Sentry v9 + Cloudflare Pattern
    
    ```ts
    import { withSentry } from '@sentry/cloudflare';
    
    export default withSentry(
      (env) => ({
        dsn: env.SENTRY_DSN,
        tracesSampleRate: 1.0,
        sendDefaultPii: false,
        release: env.SENTRY_RELEASE,
        environment: env.ENVIRONMENT,
      }),
      {
        async fetch(request, env, ctx) {
          // your worker
        },
      },
    );
    ```
    
    Use **OIDC** to mint `CLOUDFLARE_API_TOKEN` in GitHub Actions — never long-lived secret in env.
    
    ## PostHog Unified (Frontend)
    
    ```ts
    posthog.init(env.POSTHOG_TOKEN, {
      api_host: 'https://us.i.posthog.com',
      persistence: 'memory', // cookie-free
      autocapture: true,
      capture_pageview: true,
      capture_pageleave: true,
      enable_recording_console_log: true, // ties errors → session replay
    });
    ```
    
    CSP additions:
    
    ```
    script-src 'self' https://us.i.posthog.com;
    connect-src 'self' https://us.i.posthog.com https://us-assets.i.posthog.com;
    ```
    
    ## CI/CD via GitHub Actions
    
    ```yaml
    - uses: actions/checkout@v5
    - uses: cloudflare/wrangler-action@v3
      with:
        apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
        # Or OIDC:
        # accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
    ```
    
    Or use **Workers Builds** (native CF CI, simpler for Worker-only projects, auto-detects framework, preview URL per branch via `wrangler versions upload`).
    
    ## Pin versions
    
    - `actions/checkout@v5`
    - `actions/upload-artifact@v4`
    - `actions/setup-node@v4`
    - `cloudflare/wrangler-action@v3` (Wrangler 4 default)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related