Skip to content
Back to skills

Growth And Launch

ASecurity

Invite-only product launch, coupon systems, referral tracking,

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 2, 2026
ai-agentstypescriptgosqlrailsapidatabasefrontendsecurity

Works with

  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 2, 2026

npx -y skills add Canhada-Labs/ceo-orchestration --skill growth-and-launch --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Growth And Launch?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Growth And Launch
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/canhada-labs-growth-and-launch/badge)](https://www.skillsdirectory.com/skills/canhada-labs-growth-and-launch)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: growth-and-launch
description: Invite-only product launch, coupon systems, referral tracking,
  trial-to-paid conversion, and waitlist management for SaaS platforms.
  Use when implementing invite codes, early access programs, time-limited
  full-access trials, Stripe coupon/promotion codes, referral reward
  systems, waitlist prioritization, cohort-based conversion tracking,
  or anti-abuse controls for launch programs. Also use when the user
  mentions invite-only, early access, beta launch, coupons, referral,
  waitlist, trial period, launch strategy, go-to-market, or converting
  early users to paying customers. Even for questions about launch
  sequencing, who to invite first, or how to structure trials, use
  this skill. Combines with monetization-and-billing for Stripe
  integration and product-conversion-readiness for UX patterns.
owner: Growth Engineer (archetype)
inspired_by:
  - source: msitarzewski/agency-agents/engineering/engineering-rapid-prototyper.md@783f6a72bfd7f3135700ac273c619d92821b419a
    license: MIT
    relationship: deliverable_template
    authored_by: ceo-orchestration framework
    authored_at: 2026-05-06
# --- smart-loading fields (PLAN-083 Wave 0a sub-agent 0.7a) ---
domain: core
priority: 6
risk_class: medium
stack: []
context_budget_tokens: 1000
inactive_but_retained: false
repo_profile_binding:
  frontend: {active: true, priority: 5}
  engine: {active: true, priority: 7}
  fintech: {active: true, priority: 6}
  trading-readonly: {active: true, priority: 10}
  generic: {active: true, priority: 6}
activation_triggers:
  - {event: help-me-invoked, regex: "(?i)launch|growth|funnel|activation"}
---

# Growth and Launch

## Cardinal Rule

**Early access is a privilege, not a discount.** The V1 invite-only
launch exists to build a high-quality user base that provides real
feedback and converts to paid. Every mechanic — invites, trials,
referrals — must reinforce the positioning of a professional-grade
institutional tool, never a consumer app giving away freebies.

## Positioning Guardrails

{{PROJECT_NAME}} targets sophisticated professional users. Every growth mechanic
must pass this test:

```
"Would a serious professional in the target role find this appropriate?"
  YES → ship it
  NO  → kill it
```

### What This Means in Practice

| ✅ Appropriate | ❌ Kill It |
|----------------|-----------|
| "You've been granted early access" | "🎉 Congrats! You unlocked access!" |
| "Your trial includes full professional feature set" | "FREE TRIAL — ACT NOW!" |
| "Refer a colleague → extend your access" | "Share with 5 friends to unlock!" |
| Weekly intelligence briefing email | Daily streak notifications |
| "Access expires Feb 28" (clean, factual) | "⏰ Only 3 days left! Don't miss out!" |
| Invite code: `INV-INST-A7X9` | Invite code: `FREECRYPTO2026` |
| "14-day institutional evaluation period" | "14-day FREE trial!!!" |

## Architecture Overview

```
┌──────────────────────────────────────────────────────┐
│                   Launch Funnel                       │
│                                                       │
│  Waitlist → Invite Code → Sign Up → Trial (14 days)  │
│                                  ↓                    │
│                        Full Access (all tiers)        │
│                                  ↓                    │
│                    Trial Expires → Convert or Free    │
│                                                       │
│  Referral: Active user invites → both get +7 days     │
│  Coupon: Strategic partners → custom trial duration    │
└──────────────────────────────────────────────────────┘
```

### Component Map

```
Supabase:
  ├── invite_codes         — codes, limits, expiry, creator
  ├── referrals            — who invited whom, reward status
  ├── trial_periods        — user trial state, dates, source
  ├── coupons              — partner/promo codes with rules
  └── profiles.trial_*     — trial columns on existing table

Engine:
  ├── Trial-aware tier middleware (trial → full access)
  └── No new endpoints (trial is a Supabase concern)

Frontend:
  ├── /invite/:code        — invite landing page
  ├── /settings → trial    — trial status, days remaining
  └── Referral UI          — generate personal invite link
```

## 1. Invite Code System

### Schema

```sql
CREATE TABLE public.invite_codes (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  code text NOT NULL UNIQUE,
  created_by uuid REFERENCES auth.users(id),  -- null = system-generated
  type text NOT NULL DEFAULT 'standard'
    CHECK (type IN ('standard', 'partner', 'press', 'institutional')),
  max_uses int NOT NULL DEFAULT 1,
  used_count int NOT NULL DEFAULT 0,
  trial_days int NOT NULL DEFAULT 14,
  tier_during_trial text NOT NULL DEFAULT 'institutional',
  expires_at timestamptz,
  note text,  -- internal: "For [media] journalist", "[partner] partnership"
  created_at timestamptz NOT NULL DEFAULT now(),
  CONSTRAINT code_format CHECK (code ~ '^[A-Z0-9-]{6,24}$')
);

CREATE INDEX idx_invite_codes_code ON public.invite_codes(code);

-- RLS: only admins create/manage, anyone can validate
ALTER TABLE public.invite_codes ENABLE ROW LEVEL SECURITY;

CREATE POLICY "Anyone can validate codes"
  ON public.invite_codes FOR SELECT
  USING (true);

CREATE POLICY "Admins manage codes"
  ON public.invite_codes FOR ALL
  USING (public.is_admin());
```

### Code Generation Patterns

```typescript
// Code format: INV-{TYPE}-{RANDOM}
// Examples:
//   INV-INST-A7X9     — institutional invite
//   INV-PRSS-K3M2     — press/media invite
//   INV-PRTN-A7X9     — partner
//   INV-EARLY-W5N8    — early access (waitlist)

function generateInviteCode(type: 'standard' | 'partner' | 'press' | 'institutional'): string {
  const prefixes = {
    standard: 'EARLY',
    partner: 'PRTN',
    press: 'PRSS',
    institutional: 'INST',
  };
  const random = randomBytes(2).toString('hex').toUpperCase();
  return `INV-${prefixes[type]}-${random}`;
}
```

### Code Validation (Signup Flow)

```typescript
async function validateInviteCode(code: string): Promise<{
  valid: boolean;
  trial_days?: number;
  tier?: string;
  error?: string;
}> {
  const { data: invite } = await supabase
    .from('invite_codes')
    .select('*')
    .eq('code', code.toUpperCase().trim())
    .single();

  if (!invite) return { valid: false, error: 'invalid_code' };
  if (invite.expires_at && new Date(invite.expires_at) < new Date()) {
    return { valid: false, error: 'code_expired' };
  }
  if (invite.used_count >= invite.max_uses) {
    return { valid: false, error: 'code_exhausted' };
  }

  return {
    valid: true,
    trial_days: invite.trial_days,
    tier: invite.tier_during_trial,
  };
}
```

### Code Redemption (Post-Signup)

```typescript
async function redeemInviteCode(userId: string, code: string): Promise<void> {
  const validation = await validateInviteCode(code);
  if (!validation.valid) throw new Error(validation.error);

  const trialEnd = new Date();
  trialEnd.setDate(trialEnd.getDate() + (validation.trial_days ?? 14));

  // Start trial
  await supabase.from('profiles').update({
    tier: validation.tier ?? 'institutional',
    trial_active: true,
    trial_started_at: new Date().toISOString(),
    trial_ends_at: trialEnd.toISOString(),
    trial_source: 'invite',
    invite_code_used: code.toUpperCase(),
  }).eq('id', userId);

  // Record trial
  await supabase.from('trial_periods').insert({
    user_id: userId,
    source: 'invite_code',
    source_ref: code.toUpperCase(),
    tier: validation.tier ?? 'institutional',
    started_at: new Date().toISOString(),
    ends_at: trialEnd.toISOString(),
  });

  // Increment usage
  await supabase.rpc('increment_invite_usage', { invite_code: code.toUpperCase() });
}
```

### Batch Generation for Launch

```sql
-- Generate 50 standard early-access codes for waitlist
INSERT INTO public.invite_codes (code, type, max_uses, trial_days, tier_during_trial, note)
SELECT
  'INV-EARLY-' || upper(substr(md5(random()::text), 1, 4)),
  'standard',
  1,
  14,
  'institutional',
  'Waitlist batch — Feb 2026'
FROM generate_series(1, 50);

-- Generate 5 partner codes (multi-use, 30-day trial)
INSERT INTO public.invite_codes (code, type, max_uses, trial_days, tier_during_trial, note)
VALUES
  ('INV-PRTN-001', 'partner', 20, 30, 'institutional', 'Partner A'),
  ('INV-PRTN-002', 'partner', 20, 30, 'institutional', 'Partner B'),
  ('INV-PRTN-003', 'partner', 20, 30, 'institutional', 'Partner C'),
  ('INV-PRTN-004', 'partner', 10, 21, 'institutional', 'Media outlet A'),
  ('INV-PRTN-005', 'partner', 10, 21, 'institutional', 'Media outlet B');
```

## 2. Trial System

### Schema Additions to Profiles

```sql
ALTER TABLE public.profiles
  ADD COLUMN IF NOT EXISTS trial_active boolean DEFAULT false,
  ADD COLUMN IF NOT EXISTS trial_started_at timestamptz,
  ADD COLUMN IF NOT EXISTS trial_ends_at timestamptz,
  ADD COLUMN IF NOT EXISTS trial_source text,
  ADD COLUMN IF NOT EXISTS invite_code_used text;
```

### Trial Period Tracking

```sql
CREATE TABLE public.trial_periods (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  source text NOT NULL CHECK (source IN (
    'invite_code', 'referral_bonus', 'coupon', 'manual'
  )),
  source_ref text,      -- invite code, referral user_id, coupon code
  tier text NOT NULL,
  started_at timestamptz NOT NULL,
  ends_at timestamptz NOT NULL,
  converted_at timestamptz,  -- when user subscribed (null = didn't convert)
  created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX idx_trial_periods_user ON public.trial_periods(user_id);
CREATE INDEX idx_trial_periods_ends ON public.trial_periods(ends_at);

ALTER TABLE public.trial_periods ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own trials"
  ON public.trial_periods FOR SELECT
  USING (auth.uid() = user_id);
```

### Tier Resolution Logic

The Engine (or Supabase RLS) must resolve the effective tier
considering both subscriptions AND active trials:

```typescript
function resolveEffectiveTier(profile: Profile): string {
  // 1. Active Stripe subscription takes priority
  if (profile.stripe_subscription_status === 'active') {
    return profile.tier;  // from Stripe webhook sync
  }

  // 2. Active trial
  if (profile.trial_active && profile.trial_ends_at) {
    const trialEnd = new Date(profile.trial_ends_at);
    if (trialEnd > new Date()) {
      return profile.tier;  // tier set during trial activation
    }
    // Trial expired — downgrade
    return 'free';
  }

  // 3. Default
  return 'free';
}
```

### Supabase RLS Function

```sql
CREATE OR REPLACE FUNCTION public.get_effective_tier(uid uuid)
RETURNS text
LANGUAGE sql STABLE SECURITY DEFINER
AS $$
  SELECT CASE
    -- Active Stripe subscription
    WHEN p.stripe_subscription_status = 'active' THEN p.tier
    -- Active trial
    WHEN p.trial_active = true AND p.trial_ends_at > now() THEN p.tier
    -- Default
    ELSE 'free'
  END
  FROM public.profiles p
  WHERE p.id = uid;
$$;
```

### Trial Expiration (Cron or Edge Function)

```typescript
// Supabase Edge Function or Engine cron: run daily
async function expireTrials() {
  const { data: expired } = await supabase
    .from('profiles')
    .select('id, trial_ends_at, invite_code_used')
    .eq('trial_active', true)
    .lt('trial_ends_at', new Date().toISOString())
    .is('stripe_subscription_id', null);  // not converted

  for (const profile of expired ?? []) {
    await supabase.from('profiles').update({
      tier: 'free',
      trial_active: false,
    }).eq('id', profile.id);

    // Record non-conversion for analytics
    await supabase.from('trial_periods').update({
      converted_at: null,  // explicit: did not convert
    })
    .eq('user_id', profile.id)
    .is('converted_at', null);
  }

  return { expired: expired?.length ?? 0 };
}
```

## 3. Referral System

### How It Works

```
User A (active trial or subscriber)
  → Generates personal invite link: {{DOMAIN}}/invite/REF-{USER_A_SHORT_ID}
  → Sends to User B

User B signs up with that link
  → User B gets 14-day trial (standard)
  → User A gets +7 days added to their trial/subscription

Rules:
  - Max 10 referrals per user (prevents abuse)
  - Referral reward only when User B verifies email
  - Self-referral detection (same IP, same email domain)
  - Reward stacks: 10 referrals = 70 extra days
```

### Schema

```sql
CREATE TABLE public.referrals (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  referrer_id uuid NOT NULL REFERENCES auth.users(id),
  referred_id uuid NOT NULL REFERENCES auth.users(id),
  referral_code text NOT NULL,
  status text NOT NULL DEFAULT 'pending'
    CHECK (status IN ('pending', 'verified', 'rewarded', 'rejected')),
  reward_days int NOT NULL DEFAULT 7,
  reward_applied_at timestamptz,
  created_at timestamptz NOT NULL DEFAULT now(),
  UNIQUE(referrer_id, referred_id)
);

CREATE INDEX idx_referrals_referrer ON public.referrals(referrer_id);
CREATE INDEX idx_referrals_code ON public.referrals(referral_code);

ALTER TABLE public.referrals ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users see own referrals"
  ON public.referrals FOR SELECT
  USING (auth.uid() = referrer_id OR auth.uid() = referred_id);
```

### Referral Code Generation

```typescript
// Personal referral codes are deterministic from user ID
function generateReferralCode(userId: string): string {
  const short = createHash('sha256')
    .update(userId)
    .digest('hex')
    .substring(0, 6)
    .toUpperCase();
  return `REF-${short}`;
}
```

### Reward Application

```typescript
async function applyReferralReward(referrerId: string, referralId: string) {
  // Check referral count limit
  const { count } = await supabase
    .from('referrals')
    .select('id', { count: 'exact', head: true })
    .eq('referrer_id', referrerId)
    .eq('status', 'rewarded');

  if ((count ?? 0) >= 10) return;  // max 10 referrals

  // Extend referrer's trial by 7 days
  const { data: profile } = await supabase
    .from('profiles')
    .select('trial_ends_at, trial_active')
    .eq('id', referrerId)
    .single();

  if (profile?.trial_active && profile.trial_ends_at) {
    const currentEnd = new Date(profile.trial_ends_at);
    currentEnd.setDate(currentEnd.getDate() + 7);

    await supabase.from('profiles').update({
      trial_ends_at: currentEnd.toISOString(),
    }).eq('id', referrerId);
  }

  // Mark referral as rewarded
  await supabase.from('referrals').update({
    status: 'rewarded',
    reward_applied_at: new Date().toISOString(),
  }).eq('referrer_id', referrerId).eq('referred_id', referralId);
}
```

### Anti-Abuse

```typescript
async function validateReferral(referrerId: string, newUserEmail: string, ip: string): Promise<{
  valid: boolean;
  reason?: string;
}> {
  // 1. Self-referral by email domain
  const { data: referrer } = await supabase
    .from('profiles')
    .select('email')
    .eq('id', referrerId)
    .single();

  if (referrer?.email) {
    const referrerDomain = referrer.email.split('@')[1];
    const newDomain = newUserEmail.split('@')[1];
    // Block same non-public domain (allow gmail, hotmail, etc.)
    const publicDomains = ['gmail.com', 'hotmail.com', 'outlook.com', 'yahoo.com', 'protonmail.com'];
    if (referrerDomain === newDomain && !publicDomains.includes(referrerDomain)) {
      return { valid: false, reason: 'same_corporate_domain' };
    }
  }

  // 2. Referral limit
  const { count } = await supabase
    .from('referrals')
    .select('id', { count: 'exact', head: true })
    .eq('referrer_id', referrerId);

  if ((count ?? 0) >= 10) {
    return { valid: false, reason: 'referral_limit_reached' };
  }

  return { valid: true };
}
```

## 4. Coupon System

### Stripe-Native Coupons

Use Stripe Promotion Codes (not custom discount logic).
This ensures coupons work correctly with the subscription lifecycle.

```typescript
// Create partner coupons via Stripe API
const coupon = await stripe.coupons.create({
  duration: 'once',
  percent_off: 100,        // 100% off first month = free trial
  max_redemptions: 20,
  metadata: {
    partner: 'hashdex',
    campaign: 'launch_feb_2026',
  },
});

const promoCode = await stripe.promotionCodes.create({
  coupon: coupon.id,
  code: 'HASHDEX2026',       // clean, professional code
  max_redemptions: 20,
  expires_at: Math.floor(Date.now() / 1000) + 90 * 86400, // 90 days
  metadata: {
    partner: 'hashdex',
  },
});
```

### Coupon Types

| Type | Discount | Duration | Example Code | For Whom |
|------|----------|----------|-------------|----------|
| Partner | 100% off 1st month | once | `HASHDEX2026` | Exchange partnerships |
| Press | 100% off 2 months | repeating (2) | `COINDESK2026` | Journalists reviewing product |
| Conference | 50% off 3 months | repeating (3) | `ABCRYPTO26` | Event attendees |
| Loyalty | 20% off forever | forever | — (via Customer Portal) | Long-term users |

### Tracking Coupon → Conversion

```sql
-- Add coupon tracking to profiles
ALTER TABLE public.profiles
  ADD COLUMN IF NOT EXISTS coupon_code_used text,
  ADD COLUMN IF NOT EXISTS acquisition_channel text;
  -- Values: 'organic', 'invite', 'referral', 'partner', 'press'
```

## 5. Waitlist → Invite Flow

### Waitlist Prioritization

The existing waitlist table should be enriched for prioritization:

```sql
-- If waitlist table exists, add priority columns:
ALTER TABLE public.waitlist
  ADD COLUMN IF NOT EXISTS priority int NOT NULL DEFAULT 50,
  ADD COLUMN IF NOT EXISTS persona text,
  ADD COLUMN IF NOT EXISTS invited_at timestamptz,
  ADD COLUMN IF NOT EXISTS invite_code text;

-- Priority scoring (adjust keywords to your domain):
-- 90+ : Institutional (company email, enterprise signals)
-- 70-89: Developer (mentioned API, integration, automation)
-- 50-69: Power user (mentioned domain-specific workflows)
-- 30-49: General interest
-- 10-29: Low quality (disposable email, no context)
```

### Invite Dispatch (Manual for V1)

```sql
-- Query: Next batch to invite (top 20 by priority)
SELECT id, email, priority, persona, created_at
FROM public.waitlist
WHERE invited_at IS NULL
ORDER BY priority DESC, created_at ASC
LIMIT 20;

-- After generating codes, mark as invited:
UPDATE public.waitlist
SET invited_at = now(), invite_code = 'INV-EARLY-XXXX'
WHERE id = $1;
```

### Invite Email Content (Tone Reference)

```markdown
Subject: Your {{PROJECT_NAME}} access is ready

You requested early access to {{PROJECT_NAME}}. Your account is ready.

Use this code to activate your 14-day institutional evaluation:

    INV-EARLY-A7X9

During your evaluation period, you'll have unrestricted access to:
- [feature 1 that matters to your target user]
- [feature 2 that matters to your target user]
- [feature 3 — a differentiator vs competitors]
- Historical analytics (30-day lookback)

Activate here: https://{{DOMAIN}}/signup?code=INV-EARLY-A7X9

— {{OWNER_NAME}}
Founder, {{PROJECT_NAME}}
```

**Tone rules for all communications:**
- No exclamation marks in subject lines
- No emoji in emails
- "Evaluation period", not "free trial"
- "Activate", not "claim your reward"
- Signed by a real person, not "The {{PROJECT_NAME}} Team"
- Factual list of what's included, no hype adjectives

## 6. Engagement Mechanics (Institutional Grade)

### What's Allowed

These reinforce value without cheapening the product:

**Weekly Intelligence Briefing (Email)**

```markdown
Subject: {{PROJECT_NAME}} — Week of Feb 17-23

Activity (your tracked items):
- 847 events / signals / opportunities detected
- Top result: [one-line summary of the most noteworthy item of the week]
- Trend: [week-over-week comparison of a key metric]

Platform:
- N integrations connected, 99.7% uptime
- New: [any integration, feature, or dataset added this week]

Your trial expires Feb 28. Subscribe to maintain access.
https://{{DOMAIN}}/settings#billing
```

**Usage Summary in Settings**

```typescript
// In /settings, show factual usage stats
function TrialStatus({ profile }: Props) {
  const daysLeft = differenceInDays(new Date(profile.trial_ends_at), new Date());

  return (
    <div className="rounded-lg border border-slate-800 bg-slate-900 p-4">
      <div className="flex items-center justify-between">
        <div>
          <h3 className="text-sm font-medium text-slate-200">
            Institutional Evaluation
          </h3>
          <p className="text-xs text-slate-400 mt-1">
            {daysLeft > 0
              ? `${daysLeft} days remaining`
              : 'Evaluation period ended'}
          </p>
        </div>
        <div className="text-right text-xs text-slate-500">
          <p>API calls: {profile.api_request_count}</p>
          <p>WS sessions: {profile.ws_session_count}</p>
        </div>
      </div>

      {daysLeft <= 3 && daysLeft > 0 && (
        <div className="mt-3 rounded bg-slate-800 p-2 text-xs text-slate-300">
          Your evaluation period ends {formatDate(profile.trial_ends_at)}.
          <a href="/settings#billing" className="text-emerald-400 ml-1">
            Subscribe to continue access →
          </a>
        </div>
      )}
    </div>
  );
}
```

**Referral Section in Settings**

```typescript
function ReferralSection({ userId }: Props) {
  const code = generateReferralCode(userId);
  const { data: referrals } = useQuery(referralQueries.byUser(userId));
  const rewardedCount = referrals?.filter(r => r.status === 'rewarded').length ?? 0;

  return (
    <div className="rounded-lg border border-slate-800 bg-slate-900 p-4">
      <h3 className="text-sm font-medium text-slate-200">
        Refer a Colleague
      </h3>
      <p className="text-xs text-slate-400 mt-1">
        Each verified referral extends your access by 7 days.
      </p>

      <div className="mt-3 flex items-center gap-2">
        <code className="rounded bg-slate-800 px-3 py-1.5 text-xs text-emerald-400 font-mono">
          {{DOMAIN}}/invite/{code}
        </code>
        <CopyButton text={`https://{{DOMAIN}}/invite/${code}`} />
      </div>

      <p className="mt-2 text-xs text-slate-500">
        {rewardedCount}/10 referrals · {rewardedCount * 7} bonus days earned
      </p>
    </div>
  );
}
```

### What's Forbidden

| Mechanic | Why It's Banned |
|----------|----------------|
| Achievement badges | Institutional users don't collect badges |
| Daily login streaks | Desperate retention tactic |
| Points / XP system | Irrelevant to professional tool value |
| Leaderboards | Users don't compete against each other |
| Pop-up notifications for milestones | Interrupt workflow |
| Countdown timers with urgency colors | Pressure tactic |
| "You're in the top 10% of users!" | Meaningless flattery |
| Animated confetti on signup | Consumer app behavior |
| "Share on Twitter for bonus!" | Social spam |

## 7. Conversion Tracking

### Cohort Schema

```sql
CREATE VIEW public.launch_cohort_metrics AS
SELECT
  date_trunc('week', p.created_at) AS cohort_week,
  p.trial_source,
  count(*) AS signups,
  count(*) FILTER (WHERE p.trial_active AND p.trial_ends_at > now()) AS active_trials,
  count(*) FILTER (WHERE p.stripe_subscription_status = 'active') AS converted,
  round(
    count(*) FILTER (WHERE p.stripe_subscription_status = 'active')::numeric /
    NULLIF(count(*), 0) * 100, 1
  ) AS conversion_pct
FROM public.profiles p
WHERE p.trial_started_at IS NOT NULL
GROUP BY 1, 2
ORDER BY 1 DESC;
```

### Key Metrics to Track

| Metric | Query | Target |
|--------|-------|--------|
| Invite → Signup rate | signups / invites_sent | >40% |
| Signup → Active (day 1) | users_with_1_api_call / signups | >60% |
| Trial → Paid conversion | subscribers / trial_completions | >15% |
| Referral rate | users_with_1_referral / active_users | >10% |
| Time to first value | median(first_api_call - signup_at) | <10 min |
| Days active during trial | avg(distinct_days_with_activity) | >7/14 |

## 8. Launch Sequence

### Phase 1: Inner Circle (Week 1)

```
Who: 10-15 hand-picked users
  - 3-4 institutional (target enterprise segment)
  - 3-4 developers / integrators
  - 2-3 domain journalists / thought leaders
  - 2-3 power users from waitlist

How: Personal email from {{OWNER_NAME}} with unique code
Trial: 30 days, institutional tier
Goal: Qualitative feedback, bug reports, testimonials
```

### Phase 2: Waitlist Batch (Week 3)

```
Who: Top 50 from waitlist by priority score
How: Automated invite email with unique codes
Trial: 14 days, institutional tier
Goal: Validate onboarding flow, measure activation rate
```

### Phase 3: Partner Distribution (Week 5)

```
Who: Exchange partners, media, conferences
How: Multi-use partner codes (20 uses each)
Trial: 14-30 days depending on partner
Goal: Volume, brand awareness, backlinks
```

### Phase 4: Open with Referral (Week 8+)

```
Who: Anyone (signup open, no invite required)
Trial: 7 days institutional, then free tier
Referral: Active from day 1
Goal: Organic growth, referral loop
```

## Anti-Patterns to Reject

| Anti-Pattern | Why It's Wrong | Correct Approach |
|---|---|---|
| Open signup from day 1 | No feedback quality control | Invite-only → waitlist → open |
| Same trial for everyone | No differentiation by value | 30d for VIPs, 14d standard, 7d open |
| No trial expiration enforcement | Users stay free forever | Cron job expires trials daily |
| Invite codes as "FREECRYPTO" | Cheapens institutional positioning | Clean codes: INV-INST-A7X9 |
| Gamified referrals with tiers | Consumer app mechanics | Simple: refer → +7 days, max 10 |
| Urgency-based trial expiry UI | Pressure tactic | Factual: "X days remaining" |
| Mass email blast to waitlist | Low quality, high spam risk | Batch by priority, 50 at a time |
| No conversion tracking | Flying blind | Cohort metrics from week 1 |
| Infinite trial extensions | Never converts to paid | Cap referral bonus at 70 days total |
| Coupon codes on public pages | Devalues paid plans | Partner codes shared privately only |

## Rapid Prototyping Discipline

This section governs how to build the product **before** it is ready to
launch through the mechanics above. The invite-code and trial infrastructure
described in §1-§8 is launch plumbing — it only matters once the core
product is functional. Getting the core product functional requires a
distinct discipline.

### Time-Box First, Features Second

NEVER estimate a prototype by listing features and summing time.
ALWAYS start with a fixed time-box, then cut features until the
remaining set fits.

```
CORRECT:
  Time-box: 48 hours
  Must-have: auth + one happy path + one persistence layer
  Cut everything else until 48h is achievable

WRONG:
  Feature list: auth, dashboard, notifications, referral UI, admin panel
  Estimate: "2-3 days per feature, so ~2 weeks"
  Result: 6-week prototype that nobody has validated yet
```

The 48-hour MVP template covers:
- Auth scaffold (use Clerk, Supabase Auth, or NextAuth — never roll your own for a prototype)
- One complete happy path (user can do the single most important thing)
- One persistence layer (database schema + one read + one write)
- Dummy payment stub (Stripe test mode, never production keys)

Everything else — error states, edge cases, admin tooling, email delivery,
referral mechanics — comes after at least one real user has completed the
happy path.

### Throwaway vs. Foundation: The Rewrite Gate

**Every prototype is throwaway until it passes the rewrite gate.**
NEVER promote a prototype to production by adding features around it.
Rewrite the parts that will carry production load.

The rewrite gate is a binary decision made before any user data enters the
system. Ask these questions:

| Question | Throwaway | Foundation |
|----------|-----------|------------|
| Was the schema designed for a single happy path? | yes | no |
| Are there hardcoded IDs, secrets, or magic strings? | yes | no |
| Does the auth layer skip role checks? | yes | no |
| Are error paths handled or swallowed? | swallowed | handled |
| Is there any observability (logging, error capture)? | no | yes |

**Rule:** Any "yes" in the Throwaway column → rewrite that component before
accepting real user data. Do not patch. Do not wrap. Rewrite the specific
component.

### Promote or Discard Rubric

When a prototype has been tested with at least 3 real users and you are
deciding whether to continue or rebuild, apply this rubric:

| Signal | Promote path | Discard path |
|--------|-------------|--------------|
| Core hypothesis | Validated: users completed the flow | Invalidated: users could not complete it |
| Conversion signal | ≥1 user asked "how do I pay?" | Zero payment intent after 5 sessions |
| Schema fit | Data model covers observed usage | You added ≥3 workaround columns |
| Tech debt density | ≤2 known hacks in critical paths | ≥3 hacks or any hack in auth/billing |
| Operator confidence | Team can explain every component | Any "I'm not sure how that works" |

**Promote path:** rewrite gate above → cherry-pick sound components →
add observability → harden auth → run the invite-code launch sequence.

**Discard path:** document what the prototype taught you (hypotheses
confirmed/rejected, schema decisions, UX dead-ends), delete the code,
start the next iteration with that knowledge.

NEVER let sunk-cost reasoning override a discard signal. A prototype that
taught you the wrong approach has done its job. The learning is the output,
not the code.

Files in this skill

  • SKILL-frontend.md24.9 KB
  • SKILL.md29.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…