Skip to content
Back to skills

Addon System

ASecurity

Version: 1.0

  • 18 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 4, 2026
developmentjavascripttypescriptgojavasqlreactnodetestingdatabasefrontend

Works with

  • cli

Security analysis

A100/100

Scanned September 4, 2026

npx -y skills add majiayu000/claude-skill-registry-data --skill addon-system --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Addon System?

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

Security grade badge for Addon System
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/majiayu000-addon-system-claude-skill-registry-data/badge)](https://www.skillsdirectory.com/skills/majiayu000-addon-system-claude-skill-registry-data)

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: addon-system
description: 'Version: 1.0'
---

# Addon/Feature System Development Guide

**Version:** 1.0
**Purpose:** Enforce consistent patterns when creating new features/addons with proper feature gates

## 🎯 Quick Reference

When adding a new feature/addon to the platform, you MUST:
- ✅ Define a unique FEATURE_CODE
- ✅ Create feature file in `src/lib/features/`
- ✅ Create custom hook (`useXyzFeature`)
- ✅ Create Feature Gates (User + Admin)
- ✅ Use theme system for upgrade prompts
- ✅ Register feature in database

## 📚 Architecture Overview

```
Feature System Flow:
1. Database (feature_definitions) → Feature Code
2. Studio Subscription/Addon → Active Features
3. FeatureProvider → Context with hasFeature(), canUse()
4. Feature Gates → Conditional Rendering
5. Components → Protected Features
```

## 🔧 Step-by-Step: Creating a New Addon

### Step 1: Define Feature Code

```typescript
// src/lib/features/my-feature.tsx
'use client'

export const MY_FEATURE_CODE = 'my_feature_name'
```

**Naming Convention:**
- Use snake_case: `chat_messaging`, `studio_blog`, `checkin_system`
- Be descriptive: `video_on_demand` not `vod`
- Must match database entry in `feature_definitions.code`

### Step 2: Create Custom Hook

```typescript
// src/lib/features/my-feature.tsx
import { useFeatures } from './feature-context'

export function useMyFeature() {
  const { hasFeature, canUse, loading } = useFeatures()

  return {
    // Ist das Feature aktiviert?
    isMyFeatureEnabled: hasFeature(MY_FEATURE_CODE),

    // Kann Feature genutzt werden? (Aktiv + Subscription gültig)
    canUseMyFeature: canUse(MY_FEATURE_CODE),

    // Lädt noch?
    loading: loading,

    // Feature Code für andere Components
    featureCode: MY_FEATURE_CODE
  }
}
```

**What the hook returns:**
- `isMyFeatureEnabled`: Feature exists in studio's active features
- `canUseMyFeature`: Feature exists AND subscription is active
- `loading`: True während features geladen werden
- `featureCode`: Der Feature-Code für generic components

### Step 3: Create Feature Gates

#### A) Simple Feature Gate (für User/Frontend)

```typescript
// src/lib/features/my-feature.tsx
import React from 'react'

export function MyFeatureGate({ children }: { children: React.ReactNode }) {
  const { canUseMyFeature, loading } = useMyFeature()

  // Während Laden: nichts anzeigen
  if (loading) return null

  // Feature nicht aktiv: nichts anzeigen
  if (!canUseMyFeature) return null

  return <>{children}</>
}
```

#### B) Admin Feature Gate (mit Upgrade-Hinweis)

```typescript
// src/lib/features/my-feature.tsx
import { activeTheme } from '@/config/theme'
import { H3 } from '@/components/ui/Typography'

export function AdminMyFeatureGate({
  children,
  fallback
}: {
  children: React.ReactNode
  fallback?: React.ReactNode
}) {
  const { isMyFeatureEnabled, loading } = useMyFeature()

  // Während Laden: Render children (Page hat eigene Loading-States)
  if (loading) {
    return <>{children}</>
  }

  // Custom Fallback?
  if (!isMyFeatureEnabled && fallback) {
    return <>{fallback}</>
  }

  // Feature nicht aktiv: Upgrade-Hinweis
  if (!isMyFeatureEnabled) {
    return (
      <div className="p-8 text-center">
        <div className="max-w-md mx-auto">
          <svg
            className="w-16 h-16 text-[rgb(23,23,23)] mx-auto mb-4"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={1.5}
              d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"
            />
          </svg>
          <H3 className="mb-2">
            My Feature nicht aktiviert
          </H3>
          <p className="text-[rgb(23,23,23)] mb-4">
            Dieses Feature ist in Ihrem aktuellen Tarif nicht enthalten.
          </p>
          <a
            href="/admin/einstellungen/tarife"
            className={\`inline-flex items-center px-4 py-2 bg-gradient-to-r \${activeTheme.gradient} text-white rounded-lg hover:opacity-90 transition-all\`}
          >
            Tarif upgraden
          </a>
        </div>
      </div>
    )
  }

  return <>{children}</>
}
```

### Step 4: Use in Components

#### Option A: With Custom Feature Gate

```typescript
// In your component
import { MyFeatureGate } from '@/lib/features/my-feature'

export default function MyPage() {
  return (
    <MyFeatureGate>
      {/* This only renders if feature is active */}
      <div>Feature Content</div>
    </MyFeatureGate>
  )
}
```

#### Option B: With Generic FeatureGate

```typescript
import { FeatureGate } from '@/components/features/FeatureGate'

export default function MyPage() {
  return (
    <FeatureGate feature="my_feature_name">
      <div>Feature Content</div>
    </FeatureGate>
  )
}
```

#### Option C: Conditional Rendering with Hook

```typescript
import { useMyFeature } from '@/lib/features/my-feature'

export default function MyComponent() {
  const { canUseMyFeature, loading } = useMyFeature()

  if (loading) return <LoadingSpinner />
  if (!canUseMyFeature) return null

  return <div>Feature Content</div>
}
```

### Step 5: Stripe Product erstellen

**WICHTIG:** Jedes Addon braucht ein Stripe Product, damit bei Studio-Erstellung keine neuen Produkte erstellt werden!

#### A) Stripe Product anlegen

```javascript
// Via Node.js Script oder Stripe Dashboard
require('dotenv').config({ path: '.env.local' });
const Stripe = require('stripe');
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

const product = await stripe.products.create({
  name: 'Bookicorn My Feature Name',  // Prefix "Bookicorn " für Konsistenz
  metadata: {
    feature_code: 'my_feature_name',  // Muss mit DB code übereinstimmen!
    type: 'addon'
  }
});

console.log('Product ID:', product.id);  // z.B. prod_TnXXXXXXXXXX
```

**Oder via Stripe Dashboard:**
1. Dashboard → Products → Add Product
2. Name: `Bookicorn [Feature Name]`
3. Metadata hinzufügen: `feature_code` = `my_feature_name`, `type` = `addon`

### Step 6: Database Setup

#### A) Register Feature Definition (mit Stripe Product ID!)

```sql
INSERT INTO feature_definitions (
  code,
  name,
  description,
  category,
  addon_price_monthly,
  addon_price_yearly,
  is_active,
  metadata
) VALUES (
  'my_feature_name',          -- Must match FEATURE_CODE
  'My Feature Name',
  'Description of what this feature does',
  'content',                   -- Category: core, content, marketing, etc.
  9.99,                        -- Monthly price (if sold as addon)
  99.99,                       -- Yearly price
  true,
  '{"status": "available", "stripe_product_id": "prod_TnXXXXXXXXXX"}'::jsonb
  --                          ↑ WICHTIG: Stripe Product ID hier eintragen!
);
```

#### Alternative: Bestehendes Feature updaten

```sql
UPDATE feature_definitions
SET metadata = metadata || '{"stripe_product_id": "prod_TnXXXXXXXXXX"}'::jsonb
WHERE code = 'my_feature_name';
```

#### B) Add to Subscription Plan (Optional)

```sql
-- Include feature in a plan
UPDATE subscription_plans
SET included_features = included_features || ARRAY['my_feature_name']
WHERE code = 'professional';
```

#### C) Or Create as Addon

```sql
-- Studio can buy as addon
INSERT INTO studio_feature_addons (
  studio_id,
  feature_id,
  status,
  billing_cycle,
  price_override
) VALUES (
  'studio-uuid',
  (SELECT id FROM feature_definitions WHERE code = 'my_feature_name'),
  'active',
  'monthly',
  NULL
);
```

## 📁 File Structure

```
src/
├── lib/
│   └── features/
│       ├── feature-context.tsx        # Admin/Studio Feature Provider
│       ├── member-feature-context.tsx # Member Dashboard Feature Provider (NEW)
│       ├── my-feature.tsx             # Your new feature
│       ├── chat-feature.tsx           # Example: Chat (Admin)
│       ├── blog-feature.ts            # Example: Blog
│       └── checkin-feature.tsx        # Example: Check-in
├── components/
│   └── features/
│       └── FeatureGate.tsx            # Generic Feature Gate
└── app/
    └── admin/
        └── my-feature/                # Admin pages for feature
            └── page.tsx
```

## 👤 Member Dashboard Feature Gates

**WICHTIG:** Das Member Dashboard hat ein SEPARATES Feature System (`MemberFeatureContext`), weil:
- Ein Kunde kann bei MEHREREN Studios Mitglied sein
- Features werden über ALLE Studios aggregiert
- Feature ist aktiv wenn MINDESTENS EIN Studio es hat

### MemberFeatureContext vs FeatureContext

| Aspekt | FeatureContext (Admin) | MemberFeatureContext (Member) |
|--------|------------------------|-------------------------------|
| Scope | Einzelnes Studio | Alle Studios des Users |
| Provider | `FeatureProvider` | `MemberFeatureProvider` |
| Hook | `useFeatures()` | `useMemberFeatures()` |
| Logik | Studio hat Feature? | Irgendein Studio hat Feature? |

### Member Feature Hook erstellen

```typescript
// src/lib/features/member-feature-context.tsx enthält:

// 1. Feature Codes Definition
export const MEMBER_FEATURE_CODES = {
  CHAT: 'chat_messaging',
  CHECKIN: 'checkin_system',
  // Neues Feature hier hinzufügen
  MY_FEATURE: 'my_feature_code',
} as const

// 2. Convenience Hooks existieren bereits:
export function useMemberChatFeature() { ... }
export function useMemberCheckinFeature() { ... }

// 3. Neuen Convenience Hook hinzufügen:
export function useMemberMyFeature() {
  const { hasFeature, hasFeatureInStudio, getStudiosWithFeature, loading } = useMemberFeatures()
  const featureCode = MEMBER_FEATURE_CODES.MY_FEATURE

  return {
    isMyFeatureEnabled: hasFeature(featureCode),
    hasMyFeatureInStudio: (studioId: string) => hasFeatureInStudio(featureCode, studioId),
    studiosWithMyFeature: getStudiosWithFeature(featureCode),
    loading,
    featureCode
  }
}
```

### Member Feature Gate erstellen

```typescript
// In member-feature-context.tsx oder eigene Datei

export function MemberMyFeatureGate({ children }: { children: React.ReactNode }) {
  return (
    <MemberFeatureGate feature={MEMBER_FEATURE_CODES.MY_FEATURE} silent>
      {children}
    </MemberFeatureGate>
  )
}
```

### Verwendung im Member Dashboard

```typescript
// src/app/dashboard/page.tsx oder Member-Komponenten

import { useMemberMyFeature, MEMBER_FEATURE_CODES } from '@/lib/features/member-feature-context'

export default function MemberDashboard() {
  // Option A: Mit spezifischem Hook
  const { isMyFeatureEnabled } = useMemberMyFeature()

  // Option B: Mit generischem Hook
  const { hasFeature } = useMemberFeatures()
  const hasMyFeature = hasFeature(MEMBER_FEATURE_CODES.MY_FEATURE)

  // Option C: Prüfen für spezifisches Studio
  const { hasFeatureInStudio } = useMemberFeatures()
  const studioHasFeature = hasFeatureInStudio('my_feature_code', studioId)

  return (
    <>
      {/* Bedingt rendern */}
      {isMyFeatureEnabled && (
        <MyFeatureSection />
      )}

      {/* Oder mit Gate Component */}
      <MemberMyFeatureGate>
        <MyFeatureSection />
      </MemberMyFeatureGate>
    </>
  )
}
```

### Navigation Items bedingt anzeigen

```typescript
// src/components/member/shared/MemberNavigation.tsx

export function MemberSidebar({ ... }: MemberNavigationProps) {
  // Feature von Props oder aus Context
  const hasChatAddon = props.hasChatAddon // Vom Dashboard durchgereicht

  return (
    <nav>
      {/* Immer sichtbare Items */}
      <NavItem icon={Home} label="Home" ... />
      <NavItem icon={Calendar} label="Kursplan" ... />

      {/* Bedingt sichtbar basierend auf Feature */}
      {hasChatAddon && (
        <NavItem icon={MessageSquare} label="Nachrichten" ... />
      )}
    </nav>
  )
}
```

### Wichtig: Studios mit MemberFeatureContext synchronisieren

```typescript
// src/components/member/hooks/useMemberData.ts

export function useMemberData({ userId }: UseMemberDataProps) {
  // Context für Feature Sync holen
  const { setStudios } = useMemberFeatures()

  const loadDashboardData = async () => {
    // ... Studios laden ...

    const allStudios = Array.from(allStudiosMap.values())
    setMyStudios(allStudios)

    // WICHTIG: Studios mit MemberFeatureContext synchronisieren
    setStudios(allStudios.map((s: any) => ({ id: s.id, name: s.name })))
  }
}
```

## 🎨 Complete Example: Video-on-Demand Feature

```typescript
// src/lib/features/vod-feature.tsx
'use client'
import React from 'react'
import { useFeatures } from './feature-context'
import { activeTheme } from '@/config/theme'
import { H3 } from '@/components/ui/Typography'

// 1. Define Feature Code
export const VOD_FEATURE_CODE = 'video_on_demand'

// 2. Custom Hook
export function useVodFeature() {
  const { hasFeature, canUse, loading } = useFeatures()

  return {
    isVodEnabled: hasFeature(VOD_FEATURE_CODE),
    canUseVod: canUse(VOD_FEATURE_CODE),
    loading: loading,
    featureCode: VOD_FEATURE_CODE
  }
}

// 3. User Feature Gate (simple)
export function VodFeatureGate({ children }: { children: React.ReactNode }) {
  const { canUseVod, loading } = useVodFeature()

  if (loading) return null
  if (!canUseVod) return null

  return <>{children}</>
}

// 4. Admin Feature Gate (with upgrade prompt)
export function AdminVodGate({
  children,
  fallback
}: {
  children: React.ReactNode
  fallback?: React.ReactNode
}) {
  const { isVodEnabled, loading } = useVodFeature()

  if (loading) {
    return <>{children}</>
  }

  if (!isVodEnabled && fallback) {
    return <>{fallback}</>
  }

  if (!isVodEnabled) {
    return (
      <div className="p-8 text-center">
        <div className="max-w-md mx-auto">
          <svg
            className="w-16 h-16 text-[rgb(23,23,23)] mx-auto mb-4"
            fill="none"
            stroke="currentColor"
            viewBox="0 0 24 24"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth={1.5}
              d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z"
            />
          </svg>
          <H3 className="mb-2">
            Video-on-Demand nicht aktiviert
          </H3>
          <p className="text-[rgb(23,23,23)] mb-4">
            Das VOD Feature ist in Ihrem aktuellen Tarif nicht enthalten.
          </p>
          <a
            href="/admin/einstellungen/tarife"
            className={\`inline-flex items-center px-4 py-2 bg-gradient-to-r \${activeTheme.gradient} text-white rounded-lg hover:opacity-90 transition-all\`}
          >
            Tarif upgraden
          </a>
        </div>
      </div>
    )
  }

  return <>{children}</>
}
```

**Usage in Component:**

```typescript
// app/admin/videos/page.tsx
import { AdminVodGate } from '@/lib/features/vod-feature'

export default function VideosPage() {
  return (
    <AdminVodGate>
      <div>
        {/* VOD Content here */}
      </div>
    </AdminVodGate>
  )
}
```

## ✅ Checklist: New Addon/Feature

Before submitting/completing a new feature, verify:

### Code
- [ ] Feature Code defined (`MY_FEATURE_CODE`)
- [ ] Custom hook created (`useMyFeature`)
- [ ] User Feature Gate created (`MyFeatureGate`)
- [ ] Admin Feature Gate created with upgrade prompt (`AdminMyFeatureGate`)
- [ ] Theme system used (`activeTheme.gradient`)
- [ ] Typography components used (`H3` from `@/components/ui/Typography`)
- [ ] No hardcoded colors (use `activeTheme`)
- [ ] No hardcoded text (use translations if user-facing)

### Stripe (WICHTIG!)
- [ ] Stripe Product erstellt (Name: `Bookicorn [Feature Name]`)
- [ ] Product Metadata: `feature_code` und `type: addon`
- [ ] Product ID notiert: `prod_TnXXXXXXXXXX`

### Datenbank
- [ ] Feature in `feature_definitions` registriert
- [ ] `metadata.stripe_product_id` eingetragen!
- [ ] `metadata.status` = `available`
- [ ] `addon_price_monthly` und `addon_price_yearly` gesetzt
- [ ] Feature added to plan OR available as addon

### Testing
- [ ] Tested with feature enabled
- [ ] Tested with feature disabled (shows upgrade prompt)
- [ ] Studio-Erstellung getestet: Kein neues Stripe Product erstellt

## 🚨 Common Mistakes to Avoid

### ❌ WRONG: Hardcoded Colors

```typescript
<div className="bg-blue-500">...</div>
```

### ✅ RIGHT: Use Theme

```typescript
<div className={\`bg-gradient-to-r \${activeTheme.gradient}\`}>...</div>
```

### ❌ WRONG: No Loading State

```typescript
export function MyFeatureGate({ children }) {
  const { canUseMyFeature } = useMyFeature()  // Missing loading!
  if (!canUseMyFeature) return null
  return <>{children}</>
}
```

### ✅ RIGHT: Handle Loading

```typescript
export function MyFeatureGate({ children }) {
  const { canUseMyFeature, loading } = useMyFeature()
  if (loading) return null  // ← Important!
  if (!canUseMyFeature) return null
  return <>{children}</>
}
```

### ❌ WRONG: Feature Code Mismatch

```typescript
// File: chat-feature.tsx
export const CHAT_FEATURE_CODE = 'messaging'  // ❌

// Database: feature_definitions.code = 'chat_messaging'  // ❌ Doesn't match!
```

### ✅ RIGHT: Matching Codes

```typescript
// File: chat-feature.tsx
export const CHAT_FEATURE_CODE = 'chat_messaging'  // ✅

// Database: feature_definitions.code = 'chat_messaging'  // ✅ Matches!
```

## 🔧 Feature Context Reference

The `FeatureProvider` provides these helper functions:

```typescript
const {
  // Subscription & Plan
  subscription,    // StudioSubscription | null
  plan,           // SubscriptionPlan | null

  // Features
  features,       // Set<string> - All active feature codes
  featureList,    // Feature[] - Full feature objects
  addons,         // FeatureAddon[] - Active addons

  // Limits
  limits,         // Record<string, number | null>
  usage,          // Record<string, LimitUsage>

  // Helpers
  hasFeature,     // (code: string) => boolean
  canUse,         // (code: string) => boolean
  hasLimit,       // (code: string) => boolean
  getRemainingLimit,  // (code: string) => number | null
  isNearLimit,    // (code: string, threshold?: number) => boolean
  isAtLimit,      // (code: string) => boolean

  // State
  loading,        // boolean
  error,          // string | null

  // Actions
  refreshFeatures // () => Promise<void>
} = useFeatures()
```

## 📊 Database Schema Reference

### feature_definitions

```sql
id                  uuid PRIMARY KEY
code                varchar UNIQUE         -- 'chat_messaging', 'studio_blog'
name                varchar                -- 'Chat & Messaging'
description         text
category            varchar                -- 'core', 'content', 'marketing'
addon_price_monthly numeric(10,2)          -- Monatspreis als Addon
addon_price_yearly  numeric(10,2)          -- Jahrespreis als Addon
is_active           boolean DEFAULT true
metadata            jsonb                  -- WICHTIG: Enthält stripe_product_id!
created_at          timestamptz

-- metadata Struktur:
-- {
--   "status": "available",              -- oder "coming_soon"
--   "stripe_product_id": "prod_TnXXX",  -- PFLICHT für Addons!
--   "featured": false,
--   "includes": ["Feature 1", "Feature 2"]
-- }
```

### studio_feature_addons

```sql
id              uuid PRIMARY KEY
studio_id       uuid REFERENCES studios
feature_id      uuid REFERENCES feature_definitions
status          varchar                -- 'active', 'cancelled', 'cancelling'
billing_cycle   varchar                -- 'monthly', 'yearly', 'usage'
price_override  numeric(10,2)
valid_until     timestamptz            -- For 'cancelling' status
created_at      timestamptz
```

## 🎯 When This Skill Activates

This skill should be loaded when:
- Creating a new feature/addon
- Keywords: `addon`, `feature`, `feature gate`, `subscription`
- Working in `src/lib/features/`
- Creating feature-gated pages
- Setting up premium features

---

**Remember:** Consistency is key! Every addon should follow this exact pattern.

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…