install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/addon-system" ~/.claude/skills/majiayu000-claude-skill-registry-addon-system && rm -rf "$T"
manifest:
skills/data/addon-system/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
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
// src/lib/features/my-feature.tsx 'use client' export const MY_FEATURE_CODE = 'my_feature_name'
Naming Convention:
- Use snake_case:
,chat_messaging
,studio_blogcheckin_system - Be descriptive:
notvideo_on_demandvod - Must match database entry in
feature_definitions.code
Step 2: Create Custom Hook
// 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:
: Feature exists in studio's active featuresisMyFeatureEnabled
: Feature exists AND subscription is activecanUseMyFeature
: True während features geladen werdenloading
: Der Feature-Code für generic componentsfeatureCode
Step 3: Create Feature Gates
A) Simple Feature Gate (für User/Frontend)
// 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)
// 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
// 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
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
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
// 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:
- Dashboard → Products → Add Product
- Name:
Bookicorn [Feature Name] - Metadata hinzufügen:
=feature_code
,my_feature_name
=typeaddon
Step 6: Database Setup
A) Register Feature Definition (mit Stripe Product ID!)
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
UPDATE feature_definitions SET metadata = metadata || '{"stripe_product_id": "prod_TnXXXXXXXXXX"}'::jsonb WHERE code = 'my_feature_name';
B) Add to Subscription Plan (Optional)
-- 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
-- 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 | | |
| Hook | | |
| Logik | Studio hat Feature? | Irgendein Studio hat Feature? |
Member Feature Hook erstellen
// 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
// 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
// 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
// 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
// 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
// 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:
// 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 (
fromH3
)@/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:
undfeature_codetype: addon - Product ID notiert:
prod_TnXXXXXXXXXX
Datenbank
- Feature in
registriertfeature_definitions -
eingetragen!metadata.stripe_product_id -
=metadata.statusavailable -
undaddon_price_monthly
gesetztaddon_price_yearly - 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
<div className="bg-blue-500">...</div>
✅ RIGHT: Use Theme
<div className={\`bg-gradient-to-r \${activeTheme.gradient}\`}>...</div>
❌ WRONG: No Loading State
export function MyFeatureGate({ children }) { const { canUseMyFeature } = useMyFeature() // Missing loading! if (!canUseMyFeature) return null return <>{children}</> }
✅ RIGHT: Handle Loading
export function MyFeatureGate({ children }) { const { canUseMyFeature, loading } = useMyFeature() if (loading) return null // ← Important! if (!canUseMyFeature) return null return <>{children}</> }
❌ WRONG: Feature Code Mismatch
// File: chat-feature.tsx export const CHAT_FEATURE_CODE = 'messaging' // ❌ // Database: feature_definitions.code = 'chat_messaging' // ❌ Doesn't match!
✅ RIGHT: Matching Codes
// 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:
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
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
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 gatesubscription - Working in
src/lib/features/ - Creating feature-gated pages
- Setting up premium features
Remember: Consistency is key! Every addon should follow this exact pattern.