mirror of
https://github.com/linsa-io/linsa.git
synced 2026-01-12 12:20:23 +01:00
Add Stripe setup and verification tasks to flow.toml; update database schema with creator economy tables for subscriptions, tiers, products, and payouts.
This commit is contained in:
239
flow.toml
239
flow.toml
@@ -1597,3 +1597,242 @@ echo "=== Migration Complete ==="
|
|||||||
description = "Safe interactive migration for production database."
|
description = "Safe interactive migration for production database."
|
||||||
dependencies = ["node", "pnpm"]
|
dependencies = ["node", "pnpm"]
|
||||||
shortcuts = ["ms", "safe"]
|
shortcuts = ["ms", "safe"]
|
||||||
|
|
||||||
|
[[tasks]]
|
||||||
|
name = "stripe-setup"
|
||||||
|
interactive = true
|
||||||
|
command = """
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Stripe Payments Setup ==="
|
||||||
|
echo ""
|
||||||
|
echo "This will configure Stripe for production payments."
|
||||||
|
echo "You'll need:"
|
||||||
|
echo " - Stripe account (https://dashboard.stripe.com)"
|
||||||
|
echo " - Secret key (sk_live_... or sk_test_...)"
|
||||||
|
echo " - Webhook signing secret (whsec_...)"
|
||||||
|
echo " - Price ID for subscription (price_...)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd packages/web
|
||||||
|
|
||||||
|
# Check if wrangler is logged in
|
||||||
|
if ! pnpm exec wrangler whoami >/dev/null 2>&1; then
|
||||||
|
echo "Not logged into Cloudflare. Running wrangler login..."
|
||||||
|
pnpm exec wrangler login
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get existing secrets
|
||||||
|
SECRETS_OUTPUT=$(pnpm exec wrangler secret list 2>&1 || echo "")
|
||||||
|
|
||||||
|
is_secret_set() {
|
||||||
|
echo "$SECRETS_OUTPUT" | grep -q "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "=== Current Stripe Configuration ==="
|
||||||
|
echo ""
|
||||||
|
echo "Checking existing secrets..."
|
||||||
|
|
||||||
|
MISSING=0
|
||||||
|
|
||||||
|
if is_secret_set "STRIPE_SECRET_KEY"; then
|
||||||
|
echo " ✓ STRIPE_SECRET_KEY is set"
|
||||||
|
else
|
||||||
|
echo " ✗ STRIPE_SECRET_KEY is NOT set"
|
||||||
|
MISSING=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_secret_set "STRIPE_WEBHOOK_SECRET"; then
|
||||||
|
echo " ✓ STRIPE_WEBHOOK_SECRET is set"
|
||||||
|
else
|
||||||
|
echo " ✗ STRIPE_WEBHOOK_SECRET is NOT set"
|
||||||
|
MISSING=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_secret_set "STRIPE_ARCHIVE_PRICE_ID"; then
|
||||||
|
echo " ✓ STRIPE_ARCHIVE_PRICE_ID is set"
|
||||||
|
else
|
||||||
|
echo " ✗ STRIPE_ARCHIVE_PRICE_ID is NOT set"
|
||||||
|
MISSING=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MISSING" -eq 0 ]; then
|
||||||
|
echo ""
|
||||||
|
echo "All Stripe secrets are configured!"
|
||||||
|
echo ""
|
||||||
|
read -p "Do you want to update any secrets? (y/N): " UPDATE
|
||||||
|
if [ "$UPDATE" != "y" ] && [ "$UPDATE" != "Y" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Stripe Endpoints ==="
|
||||||
|
echo ""
|
||||||
|
echo "Your Stripe integration is ready:"
|
||||||
|
echo " - Checkout: POST /api/stripe/checkout"
|
||||||
|
echo " - Billing portal: POST /api/stripe/portal"
|
||||||
|
echo " - Billing status: GET /api/stripe/billing"
|
||||||
|
echo " - Webhooks: POST /api/stripe/webhooks"
|
||||||
|
echo ""
|
||||||
|
echo "Webhook URL for Stripe Dashboard:"
|
||||||
|
echo " https://linsa.io/api/stripe/webhooks"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Stripe Dashboard Setup ==="
|
||||||
|
echo ""
|
||||||
|
echo "Before continuing, ensure you have:"
|
||||||
|
echo ""
|
||||||
|
echo "1. Created a Product in Stripe Dashboard:"
|
||||||
|
echo " https://dashboard.stripe.com/products/create"
|
||||||
|
echo " - Name: 'Archive Pro' (or similar)"
|
||||||
|
echo ' - Add a recurring price (e.g., $5/month)'
|
||||||
|
echo ""
|
||||||
|
echo "2. Created a Webhook endpoint:"
|
||||||
|
echo " https://dashboard.stripe.com/webhooks/create"
|
||||||
|
echo " - URL: https://linsa.io/api/stripe/webhooks"
|
||||||
|
echo " - Events to listen for:"
|
||||||
|
echo " • checkout.session.completed"
|
||||||
|
echo " • customer.subscription.created"
|
||||||
|
echo " • customer.subscription.updated"
|
||||||
|
echo " • customer.subscription.deleted"
|
||||||
|
echo " • invoice.payment_succeeded"
|
||||||
|
echo " • invoice.payment_failed"
|
||||||
|
echo ""
|
||||||
|
read -p "Press Enter when ready to continue..."
|
||||||
|
|
||||||
|
# STRIPE_SECRET_KEY
|
||||||
|
echo ""
|
||||||
|
echo "=== STRIPE_SECRET_KEY ==="
|
||||||
|
echo "Find this at: https://dashboard.stripe.com/apikeys"
|
||||||
|
echo "Use sk_test_... for testing, sk_live_... for production"
|
||||||
|
echo ""
|
||||||
|
if is_secret_set "STRIPE_SECRET_KEY"; then
|
||||||
|
read -p "Already set. Enter new value to update (or press Enter to skip): " STRIPE_SECRET_KEY
|
||||||
|
else
|
||||||
|
read -p "Enter STRIPE_SECRET_KEY: " STRIPE_SECRET_KEY
|
||||||
|
fi
|
||||||
|
if [ -n "$STRIPE_SECRET_KEY" ]; then
|
||||||
|
echo "$STRIPE_SECRET_KEY" | pnpm exec wrangler secret put STRIPE_SECRET_KEY
|
||||||
|
echo "✓ STRIPE_SECRET_KEY set"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# STRIPE_WEBHOOK_SECRET
|
||||||
|
echo ""
|
||||||
|
echo "=== STRIPE_WEBHOOK_SECRET ==="
|
||||||
|
echo "After creating webhook, click on it to see the signing secret (whsec_...)"
|
||||||
|
echo ""
|
||||||
|
if is_secret_set "STRIPE_WEBHOOK_SECRET"; then
|
||||||
|
read -p "Already set. Enter new value to update (or press Enter to skip): " STRIPE_WEBHOOK_SECRET
|
||||||
|
else
|
||||||
|
read -p "Enter STRIPE_WEBHOOK_SECRET: " STRIPE_WEBHOOK_SECRET
|
||||||
|
fi
|
||||||
|
if [ -n "$STRIPE_WEBHOOK_SECRET" ]; then
|
||||||
|
echo "$STRIPE_WEBHOOK_SECRET" | pnpm exec wrangler secret put STRIPE_WEBHOOK_SECRET
|
||||||
|
echo "✓ STRIPE_WEBHOOK_SECRET set"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# STRIPE_ARCHIVE_PRICE_ID
|
||||||
|
echo ""
|
||||||
|
echo "=== STRIPE_ARCHIVE_PRICE_ID ==="
|
||||||
|
echo "Find this in your Product page -> Pricing section (starts with price_...)"
|
||||||
|
echo ""
|
||||||
|
if is_secret_set "STRIPE_ARCHIVE_PRICE_ID"; then
|
||||||
|
read -p "Already set. Enter new value to update (or press Enter to skip): " STRIPE_ARCHIVE_PRICE_ID
|
||||||
|
else
|
||||||
|
read -p "Enter STRIPE_ARCHIVE_PRICE_ID: " STRIPE_ARCHIVE_PRICE_ID
|
||||||
|
fi
|
||||||
|
if [ -n "$STRIPE_ARCHIVE_PRICE_ID" ]; then
|
||||||
|
echo "$STRIPE_ARCHIVE_PRICE_ID" | pnpm exec wrangler secret put STRIPE_ARCHIVE_PRICE_ID
|
||||||
|
echo "✓ STRIPE_ARCHIVE_PRICE_ID set"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Verification ==="
|
||||||
|
echo ""
|
||||||
|
SECRETS_OUTPUT=$(pnpm exec wrangler secret list 2>&1 || echo "")
|
||||||
|
|
||||||
|
check_final() {
|
||||||
|
if echo "$SECRETS_OUTPUT" | grep -q "$1"; then
|
||||||
|
echo " ✓ $1"
|
||||||
|
else
|
||||||
|
echo " ✗ $1 (MISSING)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_final "STRIPE_SECRET_KEY"
|
||||||
|
check_final "STRIPE_WEBHOOK_SECRET"
|
||||||
|
check_final "STRIPE_ARCHIVE_PRICE_ID"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Setup Complete ==="
|
||||||
|
echo ""
|
||||||
|
echo "Your Stripe integration endpoints:"
|
||||||
|
echo " - Checkout: POST /api/stripe/checkout"
|
||||||
|
echo " - Portal: POST /api/stripe/portal"
|
||||||
|
echo " - Billing: GET /api/stripe/billing"
|
||||||
|
echo " - Webhooks: POST /api/stripe/webhooks"
|
||||||
|
echo ""
|
||||||
|
echo "Webhook URL (add to Stripe Dashboard):"
|
||||||
|
echo " https://linsa.io/api/stripe/webhooks"
|
||||||
|
echo ""
|
||||||
|
echo "To test:"
|
||||||
|
echo " 1. Visit https://linsa.io/archive"
|
||||||
|
echo " 2. Click 'Subscribe' to start checkout"
|
||||||
|
echo " 3. Use test card: 4242 4242 4242 4242"
|
||||||
|
echo ""
|
||||||
|
echo "Run 'f deploy' to deploy with new secrets."
|
||||||
|
"""
|
||||||
|
description = "Configure Stripe payments for production: API keys, webhook, and price ID."
|
||||||
|
dependencies = ["node", "pnpm"]
|
||||||
|
shortcuts = ["stripe", "pay"]
|
||||||
|
|
||||||
|
[[tasks]]
|
||||||
|
name = "stripe-check"
|
||||||
|
command = """
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
echo "=== Stripe Configuration Check ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cd packages/web
|
||||||
|
|
||||||
|
# Get secrets
|
||||||
|
SECRETS_OUTPUT=$(pnpm exec wrangler secret list 2>&1 || echo "")
|
||||||
|
|
||||||
|
check() {
|
||||||
|
if echo "$SECRETS_OUTPUT" | grep -q "$1"; then
|
||||||
|
echo " ✓ $1"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo " ✗ $1 (MISSING)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
MISSING=0
|
||||||
|
|
||||||
|
echo "Stripe Secrets:"
|
||||||
|
check "STRIPE_SECRET_KEY" || MISSING=1
|
||||||
|
check "STRIPE_WEBHOOK_SECRET" || MISSING=1
|
||||||
|
check "STRIPE_ARCHIVE_PRICE_ID" || MISSING=1
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "API Endpoints:"
|
||||||
|
echo " - Checkout: POST https://linsa.io/api/stripe/checkout"
|
||||||
|
echo " - Portal: POST https://linsa.io/api/stripe/portal"
|
||||||
|
echo " - Billing: GET https://linsa.io/api/stripe/billing"
|
||||||
|
echo " - Webhooks: POST https://linsa.io/api/stripe/webhooks"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
if [ "$MISSING" -eq 1 ]; then
|
||||||
|
echo "⚠ Some secrets missing. Run 'f stripe-setup' to configure."
|
||||||
|
else
|
||||||
|
echo "✓ All Stripe secrets configured!"
|
||||||
|
echo ""
|
||||||
|
echo "To test checkout flow:"
|
||||||
|
echo " curl -X POST https://linsa.io/api/stripe/checkout -H 'Cookie: <session>'"
|
||||||
|
fi
|
||||||
|
"""
|
||||||
|
description = "Check Stripe configuration status."
|
||||||
|
dependencies = ["node", "pnpm"]
|
||||||
|
shortcuts = ["stc", "stripe-check"]
|
||||||
|
|||||||
@@ -436,3 +436,132 @@ export type ContextItem = z.infer<typeof selectContextItemSchema>
|
|||||||
export type ThreadContextItem = z.infer<typeof selectThreadContextItemSchema>
|
export type ThreadContextItem = z.infer<typeof selectThreadContextItemSchema>
|
||||||
export type BrowserSession = z.infer<typeof selectBrowserSessionSchema>
|
export type BrowserSession = z.infer<typeof selectBrowserSessionSchema>
|
||||||
export type BrowserSessionTab = z.infer<typeof selectBrowserSessionTabSchema>
|
export type BrowserSessionTab = z.infer<typeof selectBrowserSessionTabSchema>
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Creator Economy - Subscriptions & Sales
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Stripe Connect accounts for creators to receive payouts
|
||||||
|
export const stripe_connect_accounts = pgTable("stripe_connect_accounts", {
|
||||||
|
id: integer("id").primaryKey().generatedAlwaysAsIdentity(),
|
||||||
|
user_id: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.unique()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
stripe_account_id: text("stripe_account_id").notNull().unique(),
|
||||||
|
onboarding_complete: boolean("onboarding_complete").notNull().default(false),
|
||||||
|
payouts_enabled: boolean("payouts_enabled").notNull().default(false),
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Subscription tiers that creators set up
|
||||||
|
export const creator_tiers = pgTable("creator_tiers", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
creator_id: text("creator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(), // e.g., "Basic", "Pro", "VIP"
|
||||||
|
description: text("description"),
|
||||||
|
price_cents: integer("price_cents").notNull(), // Price in cents (e.g., 500 = $5)
|
||||||
|
currency: varchar("currency", { length: 3 }).notNull().default("usd"),
|
||||||
|
benefits: text("benefits"), // JSON array of benefits or plain text
|
||||||
|
stripe_price_id: text("stripe_price_id"), // Created when tier is made
|
||||||
|
is_active: boolean("is_active").notNull().default(true),
|
||||||
|
sort_order: integer("sort_order").notNull().default(0),
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Users subscribing to creators
|
||||||
|
export const creator_subscriptions = pgTable("creator_subscriptions", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
subscriber_id: text("subscriber_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
creator_id: text("creator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
tier_id: uuid("tier_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => creator_tiers.id, { onDelete: "cascade" }),
|
||||||
|
stripe_subscription_id: text("stripe_subscription_id").unique(),
|
||||||
|
status: varchar("status", { length: 32 }).notNull().default("active"), // active, canceled, past_due
|
||||||
|
current_period_start: timestamp("current_period_start", { withTimezone: true }),
|
||||||
|
current_period_end: timestamp("current_period_end", { withTimezone: true }),
|
||||||
|
cancel_at_period_end: boolean("cancel_at_period_end").default(false),
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// One-time products/items creators can sell
|
||||||
|
export const creator_products = pgTable("creator_products", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
creator_id: text("creator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
description: text("description"),
|
||||||
|
price_cents: integer("price_cents").notNull(),
|
||||||
|
currency: varchar("currency", { length: 3 }).notNull().default("usd"),
|
||||||
|
type: varchar("type", { length: 32 }).notNull().default("digital"), // digital, physical, service
|
||||||
|
// For digital products
|
||||||
|
content_url: text("content_url"), // URL to downloadable content
|
||||||
|
// For display
|
||||||
|
image_url: text("image_url"),
|
||||||
|
stripe_price_id: text("stripe_price_id"),
|
||||||
|
is_active: boolean("is_active").notNull().default(true),
|
||||||
|
stock: integer("stock"), // null = unlimited
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
updated_at: timestamp("updated_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Purchase records for one-time products
|
||||||
|
export const creator_purchases = pgTable("creator_purchases", {
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
buyer_id: text("buyer_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
product_id: uuid("product_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => creator_products.id, { onDelete: "cascade" }),
|
||||||
|
creator_id: text("creator_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
|
stripe_payment_intent_id: text("stripe_payment_intent_id"),
|
||||||
|
amount_cents: integer("amount_cents").notNull(),
|
||||||
|
currency: varchar("currency", { length: 3 }).notNull().default("usd"),
|
||||||
|
status: varchar("status", { length: 32 }).notNull().default("completed"), // pending, completed, refunded
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.defaultNow()
|
||||||
|
.notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
|
// Schema exports for creator economy
|
||||||
|
export const selectStripeConnectAccountSchema = createSelectSchema(stripe_connect_accounts)
|
||||||
|
export const selectCreatorTierSchema = createSelectSchema(creator_tiers)
|
||||||
|
export const selectCreatorSubscriptionSchema = createSelectSchema(creator_subscriptions)
|
||||||
|
export const selectCreatorProductSchema = createSelectSchema(creator_products)
|
||||||
|
export const selectCreatorPurchaseSchema = createSelectSchema(creator_purchases)
|
||||||
|
|
||||||
|
export type StripeConnectAccount = z.infer<typeof selectStripeConnectAccountSchema>
|
||||||
|
export type CreatorTier = z.infer<typeof selectCreatorTierSchema>
|
||||||
|
export type CreatorSubscription = z.infer<typeof selectCreatorSubscriptionSchema>
|
||||||
|
export type CreatorProduct = z.infer<typeof selectCreatorProductSchema>
|
||||||
|
export type CreatorPurchase = z.infer<typeof selectCreatorPurchaseSchema>
|
||||||
|
|||||||
Reference in New Issue
Block a user