Invite-only product launch, coupon systems, referral tracking,
Scanned 9/2/2026
Install to Claude Code
npx -y skills add Canhada-Labs/ceo-orchestration --skill growth-and-launch --agent claude-codeInstalls 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 automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/canhada-labs-growth-and-launch)More formats (shields.io, HTML) on the badges page.
---
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.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!