Version: 1.0
Scanned 9/4/2026
Install to Claude Code
npx -y skills add majiayu000/claude-skill-registry-data --skill addon-system --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Addon System?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/majiayu000-addon-system-claude-skill-registry-data)More formats (shields.io, HTML) on the badges page.
---
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.
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!