code/tamagui.dev/doc/subcriptions.md
This document provides a comprehensive guide to the Tamagui subscription system, covering all subscription plans, database architecture, payment flows, and implementation details.
Database Tables: subscriptions, subscription_items, products, prices
Database Tables: team_subscriptions, team_members, subscription_items
subscription_itemsDatabase Tables: subscriptions, discord_invites
Database Tables: subscriptions, subscription_items
User Purchase β create-subscription+api.ts β Stripe Subscription Creation
ποΈ Database Flow:
create-subscription+api.tsPRO_SUBSCRIPTION_PRICE_IDwebhook+api.ts handles customer.subscription.createdsubscriptions tablesubscription_items tablesubscription_items.subscription_idπ Key Code Points:
// create-subscription+api.ts
const subscription = await stripe.subscriptions.create({
customer: stripeCustomerId,
items: [{ price: PRO_SUBSCRIPTION_PRICE_ID }],
payment_behavior: 'default_incomplete',
expand: ['latest_invoice.payment_intent'],
})
User Purchase β create-subscription+api.ts β Stripe Invoice Creation
ποΈ Database Flow:
create-subscription+api.ts (when disableAutoRenew: true)PRO_ONE_TIME_PRICE_IDwebhook+api.ts handles invoice.paidsubscriptions with cancel_at_period_end: truecancel_at to one year from creationsubscription_itemsπ Key Code Points:
// webhook+api.ts - manageOneTimePayment()
await supabaseAdmin.from('subscriptions').insert({
id: invoice.id,
user_id: uuid,
status: 'active',
cancel_at: oneYearFromNow.toISOString(),
cancel_at_period_end: true,
})
User Purchase β create-subscription+api.ts β Update Existing PRO Subscription
ποΈ Database Flow:
create-subscription+api.ts (with teamSeats > 0)TEAM_SEATS_SUBSCRIPTION_PRICE_ID to existing subscriptionwebhook+api.ts handles customer.subscription.updatedsubscription_items table with team seats itemteam_subscriptions record via createTeamSubscription()team_subscriptions.total_seatsπ Key Code Points:
// create-subscription+api.ts
let items: Stripe.SubscriptionCreateParams.Item[] = [{ price: PRO_SUBSCRIPTION_PRICE_ID }]
if (teamSeatCount > 0) {
items.push({ price: TEAM_SEATS_SUBSCRIPTION_PRICE_ID, quantity: teamSeatCount })
}
ποΈ Database Tables: team_members, team_subscriptions, users
β Add Member Flow:
team-seat+api.ts POST endpointteam_members tableresend-github-invite+api.tsβ Remove Member Flow:
team-seat+api.ts DELETE endpointteam_members tableπ Logic Location: ensureSubscription.ts, Discord API endpoints
// Discord seats calculation logic
const baseSeats = subscription.quantity || 1 // PRO plan base seats
const teamSeats = teamSubscription?.total_seats || 0
const totalDiscordSeats = baseSeats + teamSeats
π Legacy Support: For old takeout prices, seats are calculated from price description parsing.
User Purchase β upgrade-subscription+api.ts β Separate Monthly Subscription
ποΈ Database Flow:
upgrade-subscription+api.tswebhook+api.ts handles subscription eventssubscriptions tableπ Key Code Points:
// upgrade-subscription+api.ts
const items: Array<{ price: string; quantity?: number }> = []
if (chatSupport) {
items.push({ price: STRIPE_PRODUCTS.CHAT.priceId })
}
if (supportTier > 0) {
items.push({ price: STRIPE_PRODUCTS.SUPPORT.priceId, quantity: supportTier })
}
ποΈ Database Tables: subscriptions (metadata), discord_invites
ποΈ Channel Creation:
π Metadata Storage:
subscriptions table metadata:{
"discord_channel": "1132001717215559691"
}
π₯ Member Management:
discord_invites table stores channel members and invitation statusπ Reset Functionality:
discord/support+api.ts and discord/channel+api.ts DELETE endpointsποΈ Database Table: product_ownership
π― Purpose:
π» Usage:
// Check legacy π± Bento access
const { data: ownership } = await supabase
.from('product_ownership')
.select('*')
.eq('user_id', userId)
.eq('product_id', BENTO_PRODUCT_ID)
π Location: ensureSubscription.ts
For old takeout subscriptions, Discord seats are calculated by parsing the price description:
// Legacy price description parsing
const description = price.description || ''
const seatsMatch = description.match(/(\d+)\s*seats?/i)
const seats = seatsMatch ? parseInt(seatsMatch[1]) : 1
π― Purpose: Add users to product_ownership for data recovery
π» Usage: When users lose access after system migration, manually add records to restore their benefits.
ποΈ Database Table: claims
π Flow:
resend-github-invite+api.ts handles invite requestscheckIfUserIsTeamMember() in github/helpers.tssubscriptions: Main subscription recordssubscription_items: Links subscriptions to products/pricesproducts: Product definitions (PRO, Team Seats, Chat, Support)prices: Pricing information for each productcustomers: Stripe customer ID mappingteam_subscriptions: Team subscription metadatateam_members: Team member relationshipsteam_invoices: Team-specific invoice trackingclaims: Repository and service access claimsproduct_ownership: Legacy one-time purchase trackingusers_private: GitHub tokens and private datasubscriptions: Discord channel metadata (stores channel IDs in JSON)discord_invites: Discord member invitations and invitation statusπ Multiple Support Subscriptions Complexity
π Discord Reset Limitations
π° One-Time Payment Limitations
π³ Stripe Payment Confirmation Issue with Large Discounts
clientSecret to use for stripe.confirmPayment()data.amount_due && data.amount_due > 0 && data.clientSecret condition before calling stripe.confirmPayment()π Consolidate Chat + Support Subscriptions
// TODO: When user has Chat and purchases Support tier,
// upgrade existing Chat subscription instead of creating new one
// Benefits: Single Discord channel, unified billing, easier management
β¨ Enhanced Discord Management
π Improved Team Seat Management
For testing user access and subscriptions, use the Supabase SSR User Impersonate Tool:
npm install
cp .env.example .env # Fill in Supabase credentials
node main.mjs --email <user-email>
This tool allows developers to impersonate any user for testing subscription flows, Discord access, and repository claims.
/api/create-subscription: Create PRO + Team Seats subscriptions/api/upgrade-subscription: Add Chat/Support tier subscriptions/api/add-team-seats: Add additional team seats to existing subscription/api/cancel-subscription: Cancel subscription with period end/api/team-seat: GET (list), POST (invite), DELETE (remove) team members/api/discord/channel: Manage general Discord channel access/api/discord/support: Manage private support channel access/api/discord/search-member: Search Discord members for invitations/api/stripe/webhook: Handle all Stripe webhook events for subscription lifecycleThis documentation provides a complete overview of the Tamagui subscription system architecture and implementation details for team members to understand and maintain the codebase effectively.