🎯

tier-entitlements

🎯Skill

from dadbodgeoff/drift

VibeIndex|
What it does

Manages subscription tier-based feature access and usage tracking with centralized configuration and atomic limit increments.

πŸ“¦

Part of

dadbodgeoff/drift(69 items)

tier-entitlements

Installation

npm installInstall npm package
npm install -g driftdetect
npm installInstall npm package
npm install -g driftdetect@latest
npm installInstall npm package
npm install -g driftdetect-mcp
Claude Desktop ConfigurationAdd this to your claude_desktop_config.json
{ "mcpServers": { "drift": { "command": "driftdetect-mcp" } } ...
πŸ“– Extracted from docs: dadbodgeoff/drift
4Installs
-
AddedFeb 4, 2026

Skill Details

SKILL.md

Implement subscription tier-based feature gating and usage limits. Centralized tier configuration, database usage tracking, and clean APIs for checking limits.

Overview

# Tier-Based Entitlements

Gate features and track usage by subscription tier.

When to Use This Skill

  • Free users get limited features
  • Pro users get unlimited access
  • Need to track daily/monthly usage
  • Want clean API for checking limits

Core Concepts

  1. Tier config - Single source of truth for all limits
  2. Usage tracking - Database tracks consumption
  3. Atomic increments - Prevent race conditions
  4. Rate limit headers - Standard HTTP headers for clients

TypeScript Implementation

Tier Configuration

```typescript

// lib/tiers.ts

export const TIER_LIMITS = {

free: {

requestsPerDay: 10,

storageGB: 1,

historyDays: 7,

exportEnabled: false,

apiAccess: false,

prioritySupport: false,

},

pro: {

requestsPerDay: Infinity,

storageGB: 100,

historyDays: 365,

exportEnabled: true,

apiAccess: true,

prioritySupport: false,

},

enterprise: {

requestsPerDay: Infinity,

storageGB: Infinity,

historyDays: Infinity,

exportEnabled: true,

apiAccess: true,

prioritySupport: true,

},

} as const;

export type UserTier = keyof typeof TIER_LIMITS;

export type TierLimits = typeof TIER_LIMITS[UserTier];

export function getTierLimits(tier: UserTier): TierLimits {

return TIER_LIMITS[tier] || TIER_LIMITS.free;

}

```

Database Schema

```sql

-- migrations/001_user_profiles.sql

CREATE TABLE user_profiles (

id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,

subscription_tier VARCHAR(20) NOT NULL DEFAULT 'free',

-- Usage tracking

requests_today INTEGER DEFAULT 0,

requests_date DATE DEFAULT CURRENT_DATE,

storage_used_bytes BIGINT DEFAULT 0,

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

);

-- Atomic increment function (prevents race conditions)

CREATE OR REPLACE FUNCTION increment_daily_usage(

p_user_id UUID,

p_today DATE

)

RETURNS TABLE(new_count INTEGER, was_reset BOOLEAN) AS $$

DECLARE

v_new_count INTEGER;

v_was_reset BOOLEAN := FALSE;

BEGIN

UPDATE user_profiles

SET

requests_today = CASE

WHEN requests_date = p_today THEN requests_today + 1

ELSE 1

END,

requests_date = p_today,

updated_at = NOW()

WHERE id = p_user_id

RETURNING requests_today INTO v_new_count;

-- Check if we reset (new day)

v_was_reset := (SELECT requests_date != p_today FROM user_profiles WHERE id = p_user_id);

RETURN QUERY SELECT v_new_count, v_was_reset;

END;

$$ LANGUAGE plpgsql;

-- Index for fast lookups

CREATE INDEX idx_user_profiles_tier ON user_profiles(subscription_tier);

```

Rate Limit Functions

```typescript

// lib/rate-limits.ts

import type { SupabaseClient } from '@supabase/supabase-js';

import { TIER_LIMITS, type UserTier } from './tiers';

export interface RateLimitResult {

allowed: boolean;

limit: number;

remaining: number;

reset: number; // Unix timestamp

tier: UserTier;

}

export async function checkRateLimit(

supabase: SupabaseClient,

userId: string

): Promise {

const today = new Date().toISOString().split('T')[0];

const { data: profile } = await supabase

.from('user_profiles')

.select('subscription_tier, requests_today, requests_date')

.eq('id', userId)

.single();

if (!profile) {

return {

allowed: true,

limit: TIER_LIMITS.free.requestsPerDay,

remaining: TIER_LIMITS.free.requestsPerDay,

reset: getResetTimestamp(),

tier: 'free',

};

}

const tier = (profile.subscription_tier as UserTier) || 'free';

const limit = TIER_LIMITS[tier].requestsPerDay;

// Unlimited tier

if (limit === Infinity) {

return {

allowed: true,

limit: -1,

remaining: -1,

reset: getResetTimestamp(),

tier,

};

}

// Check usage from today

const used = profile.requests_date === today ? profile.requests_today : 0;

const remaining = Math.max(0, limit - used);

return {

allowed: remaining > 0,

limit,

remaining,

reset: getResetTimestamp(),

tier,

};

}

export async function incrementUsage(

supabase: SupabaseClient,

userId: string

): Promise {

const today = new Date().toISOString().split('T')[0];

const { error } = await supabase.rpc('increment_daily_usage', {

p_user_id: userId,

p_today: today,

});

return !error;

}

function getResetTimestamp(): number {

const tomorrow = new Date();

tomorrow.setUTCDate(tomorrow.getUTCDate() + 1);

tomorrow.setUTCHours(0, 0, 0, 0);

return Math.floor(tomorrow.getTime() / 1000);

}

export function getRateLimitHeaders(result: RateLimitResult): Record {

const headers: Record = {

'X-RateLimit-Limit': result.limit === -1 ? 'unlimited' : String(result.limit),

'X-RateLimit-Remaining': result.remaining === -1 ? 'unlimited' : String(result.remaining),

'X-RateLimit-Reset': String(result.reset),

};

if (!result.allowed) {

const retryAfter = result.reset - Math.floor(Date.now() / 1000);

headers['Retry-After'] = String(Math.max(0, retryAfter));

}

return headers;

}

export function createRateLimitResponse(result: RateLimitResult): Response {

return new Response(

JSON.stringify({

error: 'Rate limit exceeded',

code: 'RATE_LIMIT_EXCEEDED',

message: You've used all ${result.limit} requests for today. Upgrade for unlimited access.,

reset: result.reset,

upgradeUrl: '/pricing',

}),

{

status: 429,

headers: {

'Content-Type': 'application/json',

...getRateLimitHeaders(result),

},

}

);

}

```

Using in API Routes

```typescript

// app/api/generate/route.ts

import { NextRequest, NextResponse } from 'next/server';

import { createServerSupabaseClient } from '@/lib/supabase-server';

import {

checkRateLimit,

incrementUsage,

createRateLimitResponse,

getRateLimitHeaders

} from '@/lib/rate-limits';

export async function POST(request: NextRequest) {

const userId = request.headers.get('x-user-id');

if (!userId) {

return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });

}

const supabase = await createServerSupabaseClient();

// Check rate limit BEFORE expensive operation

const rateLimit = await checkRateLimit(supabase, userId);

if (!rateLimit.allowed) {

return createRateLimitResponse(rateLimit);

}

// Process the request

const body = await request.json();

const result = await processRequest(body);

// Increment usage AFTER successful operation

await incrementUsage(supabase, userId);

return NextResponse.json(

{ result },

{ headers: getRateLimitHeaders(rateLimit) }

);

}

```

Feature Gate Component

```typescript

// components/FeatureGate.tsx

'use client';

import { useUser } from '@/hooks/useUser';

import { TIER_LIMITS, type UserTier } from '@/lib/tiers';

interface FeatureGateProps {

feature: keyof typeof TIER_LIMITS.free;

children: React.ReactNode;

fallback?: React.ReactNode;

}

export function FeatureGate({ feature, children, fallback }: FeatureGateProps) {

const { tier, isLoading } = useUser();

if (isLoading) return null;

const limits = TIER_LIMITS[tier as UserTier];

const hasAccess = limits[feature] === true || limits[feature] === Infinity;

if (!hasAccess) {

return fallback ?? (

This feature requires Pro

Upgrade now

);

}

return <>{children};

}

// Usage

feature="apiAccess"

fallback={}

>

```

Usage Display Component

```typescript

// components/UsageDisplay.tsx

'use client';

import { useUser } from '@/hooks/useUser';

import { TIER_LIMITS } from '@/lib/tiers';

export function UsageDisplay() {

const { tier, profile } = useUser();

const limits = TIER_LIMITS[tier];

if (limits.requestsPerDay === Infinity) {

return Unlimited;

}

const used = profile?.requests_today ?? 0;

const remaining = Math.max(0, limits.requestsPerDay - used);

const percentage = (used / limits.requestsPerDay) * 100;

return (

{remaining} remaining today

{used} / {limits.requestsPerDay}

className={h-full ${percentage > 80 ? 'bg-red-500' : 'bg-blue-500'}}

style={{ width: ${Math.min(100, percentage)}% }}

/>

{remaining === 0 && (

Upgrade for unlimited

)}

);

}

```

Python Implementation

```python

# lib/tiers.py

from dataclasses import dataclass

from typing import Literal

import math

TierName = Literal["free", "pro", "enterprise"]

@dataclass(frozen=True)

class TierLimits:

requests_per_day: float # Use math.inf for unlimited

storage_gb: float

history_days: int

export_enabled: bool

api_access: bool

TIER_LIMITS: dict[TierName, TierLimits] = {

"free": TierLimits(

requests_per_day=10,

storage_gb=1,

history_days=7,

export_enabled=False,

api_access=False,

),

"pro": TierLimits(

requests_per_day=math.inf,

storage_gb=100,

history_days=365,

export_enabled=True,

api_access=True,

),

}

def get_tier_limits(tier: TierName) -> TierLimits:

return TIER_LIMITS.get(tier, TIER_LIMITS["free"])

```

```python

# lib/rate_limits.py

from datetime import datetime, timezone

from dataclasses import dataclass

@dataclass

class RateLimitResult:

allowed: bool

limit: int

remaining: int

reset: int

tier: str

async def check_rate_limit(db, user_id: str) -> RateLimitResult:

today = datetime.now(timezone.utc).date().isoformat()

profile = await db.get_user_profile(user_id)

tier = profile.subscription_tier if profile else "free"

limits = get_tier_limits(tier)

if limits.requests_per_day == math.inf:

return RateLimitResult(

allowed=True,

limit=-1,

remaining=-1,

reset=get_reset_timestamp(),

tier=tier,

)

used = profile.requests_today if profile.requests_date == today else 0

remaining = max(0, int(limits.requests_per_day) - used)

return RateLimitResult(

allowed=remaining > 0,

limit=int(limits.requests_per_day),

remaining=remaining,

reset=get_reset_timestamp(),

tier=tier,

)

```

Response Headers

```

X-RateLimit-Limit: 10 # Max requests allowed

X-RateLimit-Remaining: 3 # Requests remaining

X-RateLimit-Reset: 1705363200 # Unix timestamp when limit resets

Retry-After: 3600 # Seconds until reset (only on 429)

```

Best Practices

  1. Check before work - Verify limits before expensive operations
  2. Increment after success - Only count successful requests
  3. Atomic increments - Use database functions to prevent races
  4. Include upgrade CTA - 429 responses should help users upgrade
  5. Show remaining - Display usage in UI

Common Mistakes

  • Incrementing before the operation completes
  • Race conditions with non-atomic increments
  • Not resetting counters on new day
  • Missing rate limit headers on responses
  • Blocking unlimited tier users

Related Skills

  • [Rate Limiting](../rate-limiting/)
  • [Stripe Integration](../stripe-integration/)
  • [Supabase Auth](../supabase-auth/)

More from this repository10

🎯
feature-flags🎯Skill

Enables controlled feature rollouts, A/B testing, and selective feature access through configurable flags for gradual deployment and user targeting.

🎯
design-tokens🎯Skill

Generates a comprehensive, type-safe design token system with WCAG AA color compliance and multi-framework support for consistent visual design.

🎯
file-uploads🎯Skill

Securely validates, scans, and processes file uploads with multi-stage checks, malware detection, and race condition prevention.

🎯
ai-coaching🎯Skill

Guides users through articulating creative intent by extracting structured parameters and detecting conversation readiness.

🎯
environment-config🎯Skill

Validates and centralizes environment variables with type safety, fail-fast startup checks, and multi-environment support.

🎯
community-feed🎯Skill

Generates efficient social feed with cursor pagination, trending algorithms, and engagement tracking for infinite scroll experiences.

🎯
cloud-storage🎯Skill

Enables secure, multi-tenant cloud file storage with signed URLs, direct uploads, and visibility control for user-uploaded assets.

🎯
email-service🎯Skill

Simplifies email sending, templating, and tracking with robust SMTP integration and support for multiple email providers and transactional workflows.

🎯
error-sanitization🎯Skill

Sanitizes error messages by logging full details server-side while exposing only generic, safe messages to prevent sensitive information leakage.

🎯
batch-processing🎯Skill

Optimizes database operations by collecting and batching independent records, improving throughput by 30-40% with built-in fallback processing.