VT uses a dual-source subscription system that maintains consistency between user plan assignments and formal subscription records.
- Purpose: Quick access to user's current plan
- Values:
'free','vt_plus' - Usage: Used for fast plan checks and as fallback when subscription records are missing
- Note: Free tier includes advanced features (Dark Mode, Document Processing, Thinking Mode, etc.) for logged-in users
- Purpose: Detailed subscription management with billing periods, credits, and payment provider integration
- Contains: Plan details, billing periods, credit tracking, Creem/Stripe integration
- Usage: Authoritative source for subscription status and billing information
The subscription status API (/api/subscription/status) follows this priority order:
- Primary: Check
user_subscriptionstable for formal subscription record - Fallback: If no subscription record but
users.plan_slug = 'vt_plus', treat as active VT+ - Default: If neither exists, default to free plan
/api/subscription/status- Get current subscription status/api/subscription/sync- Sync subscription data between tables/api/user/profile- User profile with subscription metadata
useSubscriptionStatus()- Database-backed subscription checking (recommended)useCreemSubscription()- Payment and portal managementuseSubscription()- Legacy hook using user metadata
subscription-sync.ts- Server-side sync utilitiessubscription.ts- Client-side subscription logic
User Subscribes → Webhook Updates user_subscriptions → API checks both sources → Frontend updates
↓
Auto-sync users.plan_slug
Possible Causes:
- Missing
user_subscriptionsrecord - Expired subscription not properly handled
- Cache/state not refreshed after subscription change
Solutions:
-
Run subscription sync:
POST /api/subscription/sync -
Check subscription status:
GET /api/subscription/status -
Verify database consistency:
SELECT u.email, u.plan_slug, us.plan, us.status, us.current_period_end FROM users u LEFT JOIN user_subscriptions us ON u.id = us.user_id WHERE u.plan_slug = 'vt_plus';
Solution: Use the sync utility to ensure consistency:
import { syncUserSubscriptionData } from '@repo/shared/utils/subscription-sync';
await syncUserSubscriptionData();- Always check both sources in subscription logic
- Use
useSubscriptionStatus()for new components (database-backed) - Sync data after subscription changes via webhooks or manual triggers
- Handle edge cases where users have plan_slug but no subscription record
- Refresh subscription state after payment flows complete
- Legacy users may have
plan_slugset but nouser_subscriptionsrecord - The sync utility automatically creates missing subscription records
- Always test subscription logic with edge cases (expired, missing records, etc.)