Payments & Billing
Nuxflare Pro includes a complete billing system powered by Stripe and Paddle.
Supported Payment Providers
- Stripe
- Paddle
Both providers are configured to work in parallel, letting you choose one or use both.
Core Components
Database Schema
export const plans = sqliteTable("Plan", {
id: text("id")
.$default(() => cuid())
.primaryKey()
.notNull(),
name: text("name").notNull(),
description: text("description").notNull(),
tier: text("tier").default("FREE").notNull(),
price: text("price").notNull(),
period: text("period").notNull(),
provider: text("provider"),
providerPriceId: text("providerPriceId"),
createdAt: text("created_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: text("updated_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
});
export const subscriptions = sqliteTable("Subscription", {
id: text("id")
.$default(() => cuid())
.primaryKey()
.notNull(),
teamId: text("teamId").notNull(),
status: text("status").default("ACTIVE").notNull(),
provider: text("provider").notNull(),
providerSubscriptionId: text("providerSubscriptionId").notNull(),
planId: text("planId").notNull(),
currentPeriodStart: integer("currentPeriodStart", { mode: "timestamp" }),
currentPeriodEnd: integer("currentPeriodEnd", { mode: "timestamp" }),
cancelAtPeriodEnd: integer("cancelAtPeriodEnd", {
mode: "boolean",
})
.default(false)
.notNull(),
providerMetadata: text("providerMetadata", { mode: "json" }),
customerEmail: text("customerEmail"),
lastPaymentDate: integer("lastPaymentDate", { mode: "timestamp" }),
nextPaymentDate: integer("nextPaymentDate", { mode: "timestamp" }),
createdAt: text("created_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`),
updatedAt: text("updated_at")
.notNull()
.default(sql`(CURRENT_TIMESTAMP)`)
.$onUpdate(() => sql`(CURRENT_TIMESTAMP)`),
});
Webhook Handlers
Located in /server/api/webhooks/
:
stripe.post.ts
- Handles Stripe subscription eventspaddle.post.ts
- Handles Paddle subscription events
Client Integration
usePayments()
composable handles client-side checkout:
const { init, checkout } = usePayments("STRIPE" | "PADDLE");
// Paddle checkout
await checkout([{ priceId: "pri_123" }], {
data: { team_id: "team_123" },
});
// Stripe checkout
const sessionUrl = await checkout("price_123", "team_123");
navigateTo(sessionUrl);
Setup Steps
- Set required secrets using SST CLI:
# Stripe
bun sst secret set StripeSecretKey sk_test_xxx
bun sst secret set StripeWebhookSecretKey whsec_xxx
# Paddle
bun sst secret set PaddleApiKey xxx
bun sst secret set PaddleWebhookSecret xxx
- Create plans in your database with provider-specific price IDs
- Set up webhook endpoints in your provider dashboards:
- Stripe:
/api/webhooks/stripe
- Paddle:
/api/webhooks/paddle
- Stripe:
- Use
billing.vue
component to display plans and handle checkouts
Creating Plans
Plans are created through the seedPlans()
function during setup.
Each plan can be configured for both Stripe and Paddle with different price IDs:
packages/app/server/tasks/seed.ts
await addPlan({
name: "Pro",
tier: "PRO",
description: "Unlock PRO features with Stripe",
period: "month",
price: "$29",
provider: "STRIPE",
providerPriceId: "price_xxx", // Your Stripe price ID
});
await addPlan({
name: "Pro",
tier: "PRO",
description: "Unlock PRO features with Paddle",
period: "month",
price: "$29",
provider: "PADDLE",
providerPriceId: "pri_xxx", // Your Paddle price ID
});
A free plan is automatically created using:
const FREE_PLAN = {
name: "Free Tier",
tier: "FREE",
description: "Get started quickly.",
price: "$0",
period: "month",
};