Payments
indiegoon supports 4 payment providers through a unified interface. Switch providers by changing one environment variable — your code stays the same.
Supported Providers
| Provider | Best For | Pricing |
|---|---|---|
| Stripe | Full control, global reach | 2.9% + $0.30 |
| Polar | Open-source monetization | 5% |
| LemonSqueezy | Simplicity, tax handling | 5% + $0.50 |
| DodoPayments | Emerging markets, low fees | Varies |
Setup
Using the CLI (Recommended)
goon setup payments
Pick your provider. The CLI will:
- •Validate your API key (makes a real API call)
- •Configure all required env vars
- •Update your
.goon/manifest.json
Manual Setup
Set PAYMENT_PROVIDER and the provider-specific keys in .env:
# Choose: stripe | polar | lemon | dodo
PAYMENT_PROVIDER="stripe"
# Stripe
STRIPE_SECRET_KEY="sk_test_..."
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="pk_test_..."
STRIPE_WEBHOOK_SECRET="whsec_..."
PRICE_ID_PRO_MONTHLY="price_..."
How It Works
The Provider Interface
Every provider implements the same interface:
interface PaymentProvider {
name: string
createCheckout(opts: CheckoutOptions): Promise<{ url: string }>
createPortal(opts: PortalOptions): Promise<{ url: string }>
listInvoices(customerId: string): Promise<Invoice[]>
parseWebhook(body: string, headers: Headers): Promise<WebhookSyncResult | null>
}
The Factory
src/lib/payments/index.ts reads PAYMENT_PROVIDER and loads the right module:
import { getPaymentProvider } from "@/lib/payments"
const provider = getPaymentProvider()
const { url } = await provider.createCheckout({
userId: session.user.id,
email: session.user.email,
priceId: "price_...",
successUrl: "/dashboard?upgraded=true",
cancelUrl: "/pricing",
})
API Routes
Three billing endpoints are pre-built:
Create Checkout Session
POST /api/billing/checkout
Body: { priceId: string }
Returns: { url: string }
Redirects the user to the payment page.
Open Customer Portal
POST /api/billing/portal
Returns: { url: string }
Lets users manage their subscription (cancel, update card, view invoices).
List Invoices
GET /api/billing/invoices
Returns: { invoices: Invoice[] }
Webhooks
Payment events (subscription created, renewed, cancelled) are handled automatically:
POST /api/webhooks/payments
The webhook route:
- •Verifies the signature (provider-specific HMAC)
- •Parses the event into a standard format
- •Syncs the subscription to your database
Webhook Verification
Each provider uses different headers for HMAC verification:
| Provider | Header | Algorithm |
|---|---|---|
| Stripe | stripe-signature | HMAC-SHA256 (multiple) |
| Polar | webhook-id + webhook-timestamp + webhook-signature | Standard Webhooks |
| LemonSqueezy | x-signature | HMAC-SHA256 |
| DodoPayments | x-dodo-signature | HMAC-SHA256 |
Subscription Database
Subscriptions are stored in the subscriptions table:
{
id: string
userId: string // FK to users
customerId: string // Provider's customer ID
subscriptionId: string // Provider's subscription ID
priceId: string // Which plan they're on
status: string // "active" | "inactive" | "cancelled" | "past_due"
currentPeriodEnd: Date // When the current period expires
cancelAtPeriodEnd: boolean
}
Using Subscription Status
Check if user has active subscription
import { db } from "@/lib/db"
import { subscriptions } from "@/lib/db/schema"
import { eq } from "drizzle-orm"
const sub = await db.query.subscriptions.findFirst({
where: eq(subscriptions.userId, userId),
})
const isPro = sub?.status === "active"
In the Billing Page
The pre-built billing page at /billing shows:
- •Current plan status
- •Upgrade buttons (links to checkout)
- •Manage subscription button (links to portal)
- •Invoice history
Switching Providers
Want to switch from Stripe to Polar? Just change the env var:
PAYMENT_PROVIDER="polar"
POLAR_ACCESS_TOKEN="your-token"
POLAR_WEBHOOK_SECRET="your-secret"
POLAR_PRODUCT_ID="your-product-id"
No code changes. The factory loads the new provider automatically.
Testing Webhooks Locally
Stripe
stripe listen --forward-to localhost:3000/api/webhooks/payments
Other Providers
Use a tunnel like ngrok:
ngrok http 3000
Then set the webhook URL in your provider's dashboard to https://your-ngrok-url.ngrok.io/api/webhooks/payments.