Claude-skill-registry addon-system

Addon/Feature System Development Guide

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.md
safety · 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_blog
    ,
    checkin_system
  • Be descriptive:
    video_on_demand
    not
    vod
  • 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:

  • 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)

// 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:

  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!)

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

AspektFeatureContext (Admin)MemberFeatureContext (Member)
ScopeEinzelnes StudioAlle Studios des Users
Provider
FeatureProvider
MemberFeatureProvider
Hook
useFeatures()
useMemberFeatures()
LogikStudio 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 (
    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

<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 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.