---
type: spec
title: Roadcrew Freemium Technical Specification
feature_id: freemium
status: Draft
version: 1.0
related_brd: ../brds/freemium-brd.md
related_prd: ../prds/freemium-prd.md
roadcrew_last_updated: "2025-10-28"
---

# ⚙️ Technical Specification: Roadcrew Freemium Model

## 1. Mapping: PRD → Spec

| Product Requirement | Spec Section | Status |
|-------------------|--------------|--------|
| R1: Universal Tier Structure | 2.1, 2.2 | ✅ Implemented (v1.6.x) |
| R2: Repository Scope Gating | 2.2, 3.1 | ✅ Implemented (v1.6.x) |
| R3: License Validation | 3.1 | ✅ Implemented (v1.6.x) |
| R4: Pause Enforcement | 3.2 | ✅ Implemented (v1.6.x) |
| R5: Hands-Off Time Measurement | 3.3 | ⚠️ Partial (needs instrumentation) |
| R6: LLM API Management | 4.1 (v1.6), 4.2 (v1.7.0+) | ✅ v1.6.x ready, 🔄 v1.7.0+ planned |

---

## 2. Technical Requirements

### 2.1 Functional Requirements

**FR1: Universal Command Set**
- All 47 Roadcrew commands available in every tier
- Tier determines feature access, not command availability
- Commands: ANALYSIS (8), PLANNING (7), RELEASE (5), IMPLEMENT (9), CODE-ANALYSIS (5), TESTING (6), PUBLISH (4), VALIDATION (3)

**FR2: Repository Scope Validation**
- Parse command-line flags: `--monorepo`, `--multi-repo`
- Validate against tier: FREE blocks both, STARTER blocks `--multi-repo`, ENTERPRISE allows both
- Return actionable error messages with upgrade links

**FR3: License Key Processing**
- Read `ROADCREW_LICENSE_KEY` environment variable
- Validate format (TBD: exact format during v1.7.0 API design)
- Return tier: 'free' (no key), 'starter', 'enterprise'

**FR4: Pause Tracking & Enforcement**
- Track items generated per command execution
- Trigger pause at: 3 items (STARTER) or 10 items (ENTERPRISE)
- Trigger pause at: 5 min elapsed (STARTER) or 20 min elapsed (ENTERPRISE)
- Respect `--force` flag to skip pause

**FR5: Timer Instrumentation**
- Record hands-on time: user input entry/exit
- Record hands-off time: autonomous processing duration
- Record decision points: pause interactions
- Store metrics in `.roadcrew/metrics/commands/YYYY-MM-DD.jsonl`

### 2.2 Non-Functional Requirements

| Requirement | Target | Notes |
|-------------|--------|-------|
| **Local Execution** | 100% local runs | No backend required (pre-API) |
| **Command Latency** | <5s CLI setup, <60s execution | Keep hands-on time minimal |
| **Success Rate** | >90% | Reliable automation |
| **Backward Compat** | v1.6.x→v1.7.0 migration smooth | Existing installs continue |
| **Scope Constraints** | FREE: 1 repo, STARTER: monorepo, ENT: multi-repo | Enforced at CLI level |

### 2.3 Out of Scope (v1.6.x)

- ❌ Server-side API (comes v1.7.0+)
- ❌ Roadcrew-managed LLM budgets (comes v1.7.0+)
- ❌ Team management (comes v1.7.0+)
- ❌ Audit logs (comes v1.7.0+)
- ❌ SSO/RBAC (future)

---

## 3. Architecture & Design

### 3.1 License Validation Component

**File:** `scripts/core/license-validator.ts`

```typescript
interface LicenseValidationResult {
  isValid: boolean;
  tier: 'free' | 'starter' | 'enterprise';
  canUseMonorepo: boolean;
  canUseMultiRepo: boolean;
  expiresAt?: Date;
  reason?: string;  // Error reason if !isValid
}

interface ScopeValidationResult {
  isValid: boolean;
  reason?: string;
  nextAction?: string;  // Upgrade link, pricing page, etc.
}

class LicenseValidator {
  validateLicense(licenseKey?: string): LicenseValidationResult
  validateScope(tier: string, requestedScopes: string[]): ScopeValidationResult
  isExpired(expiresAt: Date): boolean
}
```

**Validation Logic:**

```typescript
// No license key → FREE tier
if (!process.env.ROADCREW_LICENSE_KEY) {
  tier = 'free';
  canUseMonorepo = false;
  canUseMultiRepo = false;
}

// License key provided → Parse & validate
const key = process.env.ROADCREW_LICENSE_KEY;
if (key.startsWith('ROADCREW_STARTER_')) {
  tier = 'starter';
  canUseMonorepo = true;
  canUseMultiRepo = false;
} else if (key.startsWith('ROADCREW_ENTERPRISE_')) {
  tier = 'enterprise';
  canUseMonorepo = true;
  canUseMultiRepo = true;
}

// Check expiration
if (expiryDate < Date.now()) {
  isValid = false;
  reason = "License expired. Upgrade at roadcrew.ai/pricing";
}
```

**Integration in Commands:**

Every command that accepts tier-dependent flags:
```typescript
const validator = new LicenseValidator();
const license = validator.validateLicense(process.env.ROADCREW_LICENSE_KEY);

if (!license.isValid) {
  console.error(`❌ ${license.reason}`);
  process.exit(1);
}

const requestedScopes = parseFlags(process.argv); // ['--monorepo']
const scopeCheck = validator.validateScope(license.tier, requestedScopes);

if (!scopeCheck.isValid) {
  console.error(`❌ ${scopeCheck.reason}`);
  console.error(`   ${scopeCheck.nextAction}`);
  process.exit(1);
}

// Proceed with command execution...
```

### 3.2 Pause Enforcement Component

**File:** `scripts/core/pause-tracker.ts`

```typescript
interface PauseState {
  itemsGenerated: number;
  itemsLastPauseAt: number;
  timeSinceLastPause: number;  // ms
  tier: 'free' | 'starter' | 'enterprise';
  needsPause: boolean;
  reason?: string;
}

const PAUSE_CONFIG = {
  'free': { itemsPerPause: null, timePerPause: null },
  'starter': { itemsPerPause: 3, timePerPause: 5 * 60 * 1000 },  // 5 min
  'enterprise': { itemsPerPause: 10, timePerPause: 20 * 60 * 1000 }  // 20 min
};

class PauseTracker {
  recordItem(): { needsPause: boolean; reason?: string }
  resumeAfterPause(): void
  getState(): PauseState
  isForceOverride(): boolean
}
```

**Pause Logic:**

```typescript
recordItem() {
  this.itemsGenerated++;
  this.timeSinceLastPause = Date.now() - this.lastPauseTime;
  
  const config = PAUSE_CONFIG[this.tier];
  
  // FREE: No pauses
  if (this.tier === 'free') {
    return { needsPause: false };
  }
  
  // STARTER/ENTERPRISE: Check both conditions
  const itemsThreshold = config.itemsPerPause;
  const timeThreshold = config.timePerPause;
  
  if (this.itemsGenerated >= itemsThreshold ||
      this.timeSinceLastPause >= timeThreshold) {
    return {
      needsPause: true,
      reason: `Generated ${this.itemsGenerated} items. Review before continuing?`
    };
  }
  
  return { needsPause: false };
}

resumeAfterPause() {
  this.itemsGenerated = 0;
  this.lastPauseTime = Date.now();
}
```

**User Interaction:**

```
Generated issue #1 → Continue
Generated issue #2 → Continue
Generated issue #3 → ⏸ PAUSE

"You've generated 3 issues. Review before continuing? (y/n)"
↓
y → Continue to issue #4
n → Exit (user cancelled)
--force flag → Skip pause (continue to #4)
```

### 3.3 Timer Instrumentation Component

**File:** `scripts/core/timer.ts`

```typescript
interface TimerMetrics {
  commandName: string;
  tier: 'free' | 'starter' | 'enterprise';
  handsOnTime: number;  // ms
  handsOffTime: number;  // ms
  decisionPointsCount: number;
  pauseDurations: number[];  // ms each
  successRate: boolean;
  timestamp: string;  // ISO8601
}

class CommandTimer {
  private handsOnStart: number;
  private handsOnEnd: number;
  private handsOffStart: number;
  private decisionPoints: number = 0;
  private pauseDurations: number[] = [];

  recordHandsOnStart(): void
  recordHandsOnEnd(): void
  recordDecisionPoint(): void
  recordDecisionResume(pauseDuration: number): void
  getMetrics(): TimerMetrics
  async saveMetrics(): Promise<void>
}
```

**Integration Pattern:**

```typescript
const timer = new CommandTimer('scope-release', tier);

try {
  timer.recordHandsOnStart();
  const userInput = await promptUser("Enter epic details...");
  timer.recordHandsOnEnd();
  
  // Autonomous processing
  const issues = await generateIssues(userInput);
  
  // Pause checkpoint
  if (shouldPause) {
    timer.recordDecisionPoint();
    const approved = await promptUser("Approve issues?");
    const pauseWallClockTime = Date.now() - pauseStartTime;
    timer.recordDecisionResume(pauseWallClockTime);
  }
} finally {
  await timer.saveMetrics();
}
```

**Metrics Storage:**

Location: `.roadcrew/metrics/commands/YYYY-MM-DD.jsonl`

```jsonl
{"commandName":"scope-release","tier":"starter","handsOnTime":45000,"handsOffTime":435000,"decisionPointsCount":1,"pauseDurations":[15000],"successRate":true,"timestamp":"2025-10-28T14:23:45Z"}
{"commandName":"implement-epic","tier":"enterprise","handsOnTime":60000,"handsOffTime":1200000,"decisionPointsCount":2,"pauseDurations":[20000,25000],"successRate":true,"timestamp":"2025-10-28T14:45:30Z"}
```

**Quarterly Review Process:**

1. Aggregate metrics from `.roadcrew/metrics/` folder
2. Calculate actual vs. promised hands-off times per tier
3. If variance > 10%: Update tier boundaries
4. Document findings in git with evidence
5. Update `FREEMIUM-PRINCIPLES.md` with new data

---

## 4. Phase-Specific Implementation

### 4.1 Pre-API Phase (v1.6.x) - Current

**Validation Flow (Client-Side):**
```
User runs: roadcrew scope-release --monorepo

↓ Check ROADCREW_LICENSE_KEY env var
↓ Validate tier (free/starter/enterprise)
↓ Check if tier allows --monorepo flag
↓ If allowed: Execute command with full scope
   If denied: Show error + upgrade link + exit(1)
```

**Feature Access Matrix:**

| Feature | Detection | Validation | Enforcement |
|---------|-----------|-----------|------------|
| Monorepo (`--monorepo`) | Parse argv | Check license tier | Block if FREE |
| Multi-repo (`--multi-repo`) | Parse argv | Check license tier | Block if FREE/STARTER |
| Pause enforcement | Track items/time | Compare to tier config | Prompt user |
| LLM API | Env vars (OPENAI_KEY, CLAUDE_KEY) | Read user-provided | User manages budget |

**LLM API Management (v1.6.x):**

Users provide their own LLM API keys:
```bash
export OPENAI_API_KEY=sk-...
export ROADCREW_LICENSE_KEY=ROADCREW_STARTER_...
roadcrew scope-release --monorepo
```

- Roadcrew routes requests through user's API
- User pays directly to their LLM provider
- Roadcrew provides budgeting guidance (no enforcement)
- Zero infrastructure cost for Roadcrew

### 4.2 Post-API Phase (v1.7.0+) - Future

**Validation Flow (Server-Side):**
```
User runs: roadcrew scope-release --monorepo
        ↓
  Local CLI sends request to: POST /api/v1/commands/scope-release
        ↓
  Authorization header: Bearer <api_key>
        ↓
  Server looks up tier from API key
        ↓
  Server checks if tier allows --monorepo
        ↓
  If allowed: Process request + return results
     If denied: Return 403 Forbidden with message
```

**Feature Access (Same Scopes, Different Mechanism):**

```typescript
// Pre-API: Client-side check
if (tier === 'free' && requestedScopes.includes('--monorepo')) {
  error("Monorepo requires STARTER tier");
}

// Post-API: Server-side check
POST /api/v1/commands/scope-release
Body: { monorepo: true, ... }
Server: if (tier==='free' && monorepo) return 403
```

**LLM API Options (v1.7.0+):**

**Option A: User Brings API (Same as v1.6.x)**
```bash
export OPENAI_API_KEY=sk-...
roadcrew scope-release --monorepo
```
- Roadcrew routes through user's API
- User pays LLM provider
- Roadcrew charges subscription ($49-$999/mo)

**Option B: Roadcrew Manages Budget (New)**
```bash
roadcrew scope-release --monorepo
# Roadcrew uses managed LLM budget
# Included in subscription ($49-$999/mo)
```
- Quota enforced per tier: STARTER=100K tokens/month, ENTERPRISE=unlimited
- Roadcrew bills LLM provider, passes costs to users

**Likely:** Hybrid (users choose A or B)

---

## 5. Data Flows

### 5.1 Command Execution Flow

```
START: User runs roadcrew [command] [flags]
  ↓
[VALIDATION GATE] License Validator
  - Check ROADCREW_LICENSE_KEY
  - Validate tier
  - Check scope flags against tier
  - If invalid: Error message + exit
  ↓
[PAUSE INIT] Pause Tracker
  - Load previous state (items count, last pause time)
  - Initialize for this execution
  ↓
[TIMER START] Command Timer
  - recordHandsOnStart()
  - User provides input
  - recordHandsOnEnd()
  ↓
[AUTONOMOUS WORK] Process & Generate
  - Main command logic runs
  - Generate artifacts (issues, PRs, etc.)
  - Track items generated
  ↓
[PAUSE CHECK] Pause Tracker  
  - Check if pause needed (items or time threshold)
  - If needed: recordDecisionPoint()
  - Prompt user: "Review before continuing?"
  - User responds: y/n/--force
  - recordDecisionResume()
  ↓
[TIMER END] Command Timer
  - getMetrics()
  - saveMetrics()
  ↓
[RESULT] Return success or error
```

### 5.2 License Validation Flow

```
TIER DETERMINATION:
  No ROADCREW_LICENSE_KEY env var
    → tier = 'free'
    
  ROADCREW_LICENSE_KEY = 'ROADCREW_STARTER_...'
    → tier = 'starter'
    
  ROADCREW_LICENSE_KEY = 'ROADCREW_ENTERPRISE_...'
    → tier = 'enterprise'

SCOPE VALIDATION:
  Requested flags: ['--monorepo']
  Tier: 'free'
    → ❌ "Monorepo requires STARTER tier ($49/mo). Upgrade at roadcrew.ai/pricing"
    
  Requested flags: ['--multi-repo']
  Tier: 'starter'
    → ❌ "Multi-repo requires ENTERPRISE tier. Upgrade at roadcrew.ai/pricing"
    
  Requested flags: ['--monorepo']
  Tier: 'starter'
    → ✅ "Proceeding with monorepo scope..."
```

---

## 6. Implementation Checklist

### Pre-API Phase (v1.6.x)

- [x] License Validator component (license-validator.ts)
- [x] Scope validation logic (tier-gating)
- [x] Error messages with upgrade links
- [x] Pause Tracker component (pause-tracker.ts)
- [x] Pause enforcement (3 items / 5 min for STARTER, etc.)
- [x] `--force` flag override support
- [ ] Timer Instrumentation component (timer.ts) - **Needs work**
- [ ] Metrics storage (.roadcrew/metrics/) - **Needs work**
- [ ] Quarterly review process - **Needs process doc**

### Post-API Phase (v1.7.0+) - Planned

- [ ] Server-side license API (`POST /api/v1/license/validate`)
- [ ] API rate limiting (pause enforcement via 429 responses)
- [ ] Optional LLM budget management
- [ ] Team authentication + audit logs
- [ ] Migration guide for v1.6.x → v1.7.0+

---

## 7. Error Messages & UX

### Scope Validation Errors

```bash
# User attempts monorepo without STARTER license
$ roadcrew scope-release --monorepo
❌ Monorepo requires STARTER tier ($49/month)

Your current tier: FREE
Upgrade at: https://roadcrew.ai/pricing
Set license with: export ROADCREW_LICENSE_KEY=<your-key>

Questions? Join our Discord: discord.gg/roadcrew
```

```bash
# User attempts multi-repo without ENTERPRISE license
$ roadcrew scope-release --multi-repo
❌ Multi-repo requires ENTERPRISE tier

Your current tier: STARTER
Upgrade at: https://roadcrew.ai/pricing
Contact sales: sales@roadcrew.ai

Questions? Join our Discord: discord.gg/roadcrew
```

### Pause Enforcement Messages

```bash
# STARTER tier: pause after 3 items
Generated issue #1 ✓
Generated issue #2 ✓
Generated issue #3 ✓

⏸ Review checkpoint reached

You've created 3 issues. Review before continuing?

→ Yes, continue to next batch (y)
→ No, stop here (n)
→ Yes, continue without pausing again (--force)

Enter choice (y/n): y
```

### License Error Messages

```bash
$ roadcrew analyze-repo
❌ Invalid license key

ROADCREW_LICENSE_KEY is set but invalid or expired.

Current value: ROADCREW_STARTER_...

Options:
1. Check key: https://dashboard.roadcrew.ai
2. Renew license: https://roadcrew.ai/billing
3. Try without key (FREE tier only): unset ROADCREW_LICENSE_KEY

Set license with: export ROADCREW_LICENSE_KEY=<new-key>
```

---

## 8. Configuration Files

### 8.1 `.roadcrew/config.json` (New)

```json
{
  "version": "1.0",
  "tier": "auto-detect",  // Will read from ROADCREW_LICENSE_KEY
  "metrics": {
    "enabled": true,
    "storage": ".roadcrew/metrics",
    "retention": "90 days"
  },
  "pause_enforcement": {
    "enabled": true,
    "override_with_force_flag": true
  },
  "commands": {
    "available": 47,
    "organized_by": "use-case-patterns"
  }
}
```

### 8.2 `.roadcrew/metrics/commands/YYYY-MM-DD.jsonl`

One JSON object per line, one file per day:

```jsonl
{"commandName":"scope-release","tier":"starter","handsOnTime":45000,"handsOffTime":435000,"decisionPointsCount":1,"pauseDurations":[15000],"successRate":true,"timestamp":"2025-10-28T14:23:45Z"}
{"commandName":"implement-epic","tier":"enterprise","handsOnTime":60000,"handsOffTime":1200000,"decisionPointsCount":2,"pauseDurations":[20000,25000],"successRate":true,"timestamp":"2025-10-28T14:45:30Z"}
```

---

## 9. Testing Strategy

| Test Case | Input | Expected Output | Tier |
|-----------|-------|-----------------|------|
| No license key | No ROADCREW_LICENSE_KEY | FREE tier detected | ALL |
| Valid STARTER key | ROADCREW_LICENSE_KEY=ROADCREW_STARTER_... | STARTER tier detected | STARTER |
| Expired license | ROADCREW_LICENSE_KEY=ROADCREW_STARTER_... (expired) | Error: "License expired" | STARTER |
| Monorepo on FREE | scope-release --monorepo (FREE) | Error: "Requires STARTER" | FREE |
| Monorepo on STARTER | scope-release --monorepo (STARTER) | ✅ Proceeds | STARTER |
| Multi-repo on STARTER | scope-release --multi-repo (STARTER) | Error: "Requires ENTERPRISE" | STARTER |
| Multi-repo on ENTERPRISE | scope-release --multi-repo (ENTERPRISE) | ✅ Proceeds | ENTERPRISE |
| Pause at 3 items | Generate 3 issues (STARTER) | ⏸ Pause prompt | STARTER |
| Pause override | Generate 3 issues --force (STARTER) | ✅ No pause | STARTER |
| Metrics recording | Run command, check `.roadcrew/metrics/` | ✅ Metrics file created | ALL |

---

## 10. Transition: v1.6.x → v1.7.0+

### User Experience (Unchanged)

Users don't see a breaking change. They upgrade Roadcrew, set same API key (or get new one from server), and continue.

```bash
# v1.6.x workflow
export ROADCREW_LICENSE_KEY=ROADCREW_STARTER_...
roadcrew scope-release --monorepo

# v1.7.0+ workflow (looks the same to user)
export ROADCREW_LICENSE_KEY=ROADCREW_STARTER_...
roadcrew scope-release --monorepo
# Internally: CLI calls API instead of local validation
```

### Migration Checklist

- [ ] Document migration guide
- [ ] Provide API key migration tool (if format changes)
- [ ] Support both v1.6.x and v1.7.0+ keys during transition
- [ ] Grandfather existing users (no forced upgrades)
- [ ] Test backward compatibility thoroughly
