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.

Nuxflare Payment and Billing Page Screenshot

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 events
  • paddle.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

  1. 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
  1. Create plans in your database with provider-specific price IDs
  2. Set up webhook endpoints in your provider dashboards:
    • Stripe: /api/webhooks/stripe
    • Paddle: /api/webhooks/paddle
  3. 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",
};