Skip to main content

Setting Up Billing for Your Users

Enable subscriptions and payments for the organizations using your kuploy-cloud platform. This guide walks you through connecting Stripe and creating the plans your end users will subscribe to.

Instance Plans vs. Platform Plans

The plans you configure here are instance plans — the subscriptions your customers (organizations) pay to you. These are separate from your platform plan at kuploy.app.

You control the pricing, limits, and names of instance plans.

Single payment surface

All payments — Stripe cards, Apple/Google Pay, and mobile money (Orange Money, MTN MoMo, KULU) — flow through the Payment Gateway on Business+ plans. This guide stops at creating a Stripe account and defining your plans; wiring up the keys and webhook happens once, on the Payment Gateway admin page, not per rail.

1. Create a Stripe Account

If you don't have a Stripe account yet:

  1. Go to stripe.com and click Start now
  2. Enter your email and create a password
  3. Verify your email address
  4. Complete business verification (can be done later for testing)
Test Mode

Stripe provides a Test mode for development. Toggle "Test mode" in the top-right of the Stripe Dashboard. Test mode uses fake money and test card numbers - perfect for setup and testing.

2. Get Your API Keys

  1. In Stripe Dashboard, click Developers in the left sidebar
  2. Click API keys
  3. You'll see two keys:
    • Publishable key (pk_test_... or pk_live_...) - safe to expose
    • Secret key (sk_test_... or sk_live_...) - keep private!
  4. Click Reveal test key to copy your secret key
caution

Never share your secret key. Never commit it to git. Only enter it in secure settings.

3. Stripe Products Are Created Automatically

You do not need to manually create Products or Prices in Stripe. When kuploy-cloud receives the plan catalog from kuploy.app during license sync, it automatically creates (or updates) matching Stripe Products and Prices using the pricing you defined in kuploy.app.

kuploy.app plan template              kuploy-cloud (on sync)
┌───────────────────────┐ ┌─────────────────────────────┐
│ slug: "pro" │ │ upsert cloud_plans │
│ priceCents: 2000 │──sync────► │ └─► Stripe API: │
│ currency: "cad" │ │ Create/update Product │
│ billingCycle: "monthly│ │ Create/update Price │
└───────────────────────┘ └─────────────────────────────┘

If you change a plan's price in kuploy.app, the next sync updates the Stripe Price on kuploy-cloud automatically. Existing subscriptions are not retroactively changed — only new checkouts use the updated price.

Default Plan

Your default plan (the one marked as default in kuploy.app) does not get a Stripe Price — it's assigned automatically to new organizations without payment processing.

4. Connect Stripe via the Payment Gateway

Your Stripe credentials live on a Provider record inside your epay-gateway deployment, not in a kuploy-cloud-only settings screen. This keeps one source of truth for every payment rail.

Follow the Payment Gateway setup guide to:

  • Connect kuploy-cloud to your epay-gateway deployment
  • Create a Stripe provider record on the gateway with your secret key (sk_test_... / sk_live_...) and webhook signing secret (whsec_...)
  • Point the Stripe webhook endpoint at the gateway (not directly at kuploy-cloud — the old /api/webhooks/stripe kuploy-cloud endpoint was removed in the cutover)

Once the gateway is connected, the Payment Gateway panel at Admin → Payment Gateway lights up "configured", and plan checkouts / billing portal / invoice listing all work without extra setup.

5. Define Your Plans

Plans are created in kuploy.app under Customers > Plans. Each plan has explicit resource limits and feature flags — you set every value yourself.

When creating a plan, the form provides:

  • Size presets (Small, Medium, Large) — pre-fill limits with sensible starting values. These are not tied to your platform subscription tier.
  • Copy from plan — duplicate an existing plan's limits and features, then adjust.
  • Manual entry — set each limit (projects, apps, databases, storage, etc.) and feature toggle individually.

All three approaches produce the same result: a plan with fully explicit limits. There is no hidden inheritance from platform plans.

After creating plans, they automatically sync to every kuploy-cloud instance during license sync. Pricing, limits, and features are all controlled from kuploy.app — no per-instance configuration needed.

6. Verify Your Plans

After the first sync completes:

  1. In kuploy-cloud, go to SettingsPlans
  2. Verify that your plans appear with the correct names and pricing
  3. Stripe Products and Prices are auto-provisioned — no manual Price ID entry required
tip

The default plan (marked in kuploy.app) does not get a Stripe Price — it's assigned automatically to new organizations without payment.

7. Set Up Webhooks

Stripe webhooks now flow Stripe → your epay-gateway → kuploy-cloud rather than directly to kuploy-cloud. Full setup is covered in the Payment Gateway guide — see Point your webhooks at kuploy-cloud.

At a glance:

  1. In the Stripe Dashboard, add an endpoint pointing at your epay-gateway:
    https://<your-gateway-host>/api/webhooks/stripe/<merchantCode>
  2. Subscribe to the subscription + invoice events (customer.subscription.*, invoice.payment_*).
  3. Paste the Stripe signing secret (whsec_...) into the Stripe provider record on your gateway — not into kuploy-cloud. The gateway verifies and relays to /api/webhooks/epay on kuploy-cloud.

8. Test Your Setup

Use Stripe test cards to verify everything works:

Card NumberResult
4242 4242 4242 4242Successful payment
4000 0000 0000 0002Card declined
4000 0000 0000 3220Requires 3D Secure

Use any future expiry date and any 3-digit CVC.

  1. Create a test user account
  2. Go to Billing and select a paid plan
  3. Complete checkout with a test card
  4. Verify the subscription appears in both kuploy-cloud and Stripe Dashboard

Assigning Plans Manually (Employees, Partners, Test Accounts)

Some organizations on your platform should not be billed through Stripe — internal employees, partner accounts, or test accounts you provision yourself. For these, use Admin → Organizations & Plans in your kuploy-cloud dashboard:

  1. Search for the organization
  2. Click Change plan and pick a plan from the dropdown
  3. Save — the org now has an "active" subscription tagged Manual with a 30-day billing period (so build-minute quotas reset on a predictable boundary)

Manual subscriptions show a Manual badge in the org list. Their members see a "Your plan is managed by the platform administrator" notice on the billing page instead of Stripe checkout/portal buttons. Use Reset period to roll the billing period forward another 30 days, and Cancel to mark the manual sub as canceled.

Stripe-backed subs are read-only here

The Organizations admin refuses to change plans on Stripe-backed subscriptions to avoid drift between the database and Stripe. To change those, use the Stripe Dashboard — webhooks will sync the change back.

Going Live

When ready for production:

  1. Complete Stripe business verification
  2. Toggle off "Test mode" in the Stripe Dashboard
  3. Swap the Stripe secret key + webhook signing secret on your epay-gateway Stripe provider record for the live values
  4. Update the Stripe webhook endpoint in the Stripe Dashboard to point at your production epay-gateway URL (test-mode and live-mode endpoints are separate)
  5. Trigger a license sync — kuploy-cloud auto-creates live Stripe Products/Prices from the plan catalog
  6. Test with a real card (you can refund immediately)

kuploy-cloud itself has no Stripe keys to swap; the gateway is the single credentials surface.


Technical Reference

For Kubernetes deployments, the only Stripe-related environment variables live on your epay-gateway deployment, not on kuploy-cloud. See admin/.env.example in the gateway repo for the full list.

kuploy-cloud only needs the gateway connection details (URL + admin key + merchant code + merchant key). Those are configured through Admin → Payment Gateway; for headless Kubernetes installs, the same four values are accepted as env vars:

VariableDescription
EPAY_GATEWAY_URLPublic URL of your epay-gateway deployment
EPAY_GATEWAY_ADMIN_KEYBearer token kuploy-cloud uses to call the gateway's management API
EPAY_MERCHANT_CODETenant merchant identifier inside the gateway
EPAY_MERCHANT_KEYHMAC secret used to verify relayed webhooks

Apply the usual way:

echo -n "your_admin_key" | base64
kubectl patch secret kuploy-secrets -n kuploy \
-p '{"data":{"EPAY_GATEWAY_ADMIN_KEY":"<base64-value>"}}'
kubectl rollout restart deployment/kuploy -n kuploy

Bootstrap Seeding

Plans are automatically synced from kuploy.app during license sync. The seed is only needed for first boot before the first sync completes:

# Run seed script (creates bootstrap plans that will be replaced on first sync)
kubectl exec -it deployment/kuploy -n kuploy -- npm run db:seed

After the first license sync, the plan catalog from kuploy.app replaces the bootstrap plans. Plan names, limits, pricing, and Stripe Products/Prices are all managed automatically — no manual Stripe configuration per instance.

Database Migrations

kuploy-cloud uses sequential Drizzle migrations (not drizzle-kit push). Migrations run automatically on every container startup via the Docker entrypoint — no manual steps needed.

Never use db:push in production

kuploy-cloud shares the same PostgreSQL database as kuploy core. drizzle-kit push only knows about cloud_* tables and would delete all kuploy core tables. Always let the automatic migration runner handle schema changes.

cloud_plans Schema Reference

After plan sync, each plan in kuploy-cloud has the following columns:

ColumnTypeDescription
planIdtext (PK)Plan slug from kuploy.app template
nametextDisplay name
stripeProductIdtextAuto-provisioned Stripe Product ID
stripePriceIdtextAuto-provisioned Stripe Price ID
priceintegerPrice in cents
currencytextCurrency code (e.g., "cad", "usd")
billingCycletext"monthly" or "annual"
maxProjectsintegerProject limit
maxAppsintegerApp limit
maxDatabasesintegerDatabase limit
maxTeamMembersintegerTeam member limit
maxDomainsintegerTotal domain limit
maxCustomDomainsintegerCustom domain limit
maxFreeSubdomainsintegerFree subdomain limit
maxPurchasableDomainsintegerPurchasable domain limit
maxInstancesintegerInstance limit
maxStorageBytesintegerStorage limit in bytes
buildMinutesPerMonthintegerMonthly build minute limit
featuresjsonbFeature flags (whiteLabel, customDomains, domainPurchase, aiAssistant, multiServer, prioritySupport)
isDefaultbooleanWhether this is the default plan
isActivebooleanSoft-delete flag
descriptiontextPlan description

Troubleshooting

Checkout fails

  1. Open Admin → Payment Gateway in kuploy-cloud and click Test Connection — a green check confirms kuploy-cloud can reach the gateway with the stored admin key.
  2. On your epay-gateway admin, verify the Stripe provider record has the right secret key for the current mode (test vs live).
  3. Check that plan sync completed and Stripe Prices were auto-provisioned:
    kubectl logs -n kuploy -l app.kubernetes.io/name=kuploy | grep -i "stripe.*price"

Webhooks not received

  1. Confirm the Stripe Dashboard endpoint points at your epay-gateway (https://<gateway>/api/webhooks/stripe/<merchantCode>) — not at kuploy-cloud.
  2. On the gateway's Stripe provider record, the apiSecret must match the Stripe Dashboard's signing secret for that endpoint.
  3. kuploy-cloud's inbound endpoint is /api/webhooks/epay; check those logs for signature/verification errors.
  4. View Stripe-side delivery logs: Stripe Dashboard → Developers → Webhooks → Select endpoint.

Plans not showing

  1. Verify license sync is running (plans are synced from kuploy.app):
    kubectl logs -n kuploy -l app.kubernetes.io/name=kuploy | grep "plan catalog"
  2. Check plans exist locally:
    kubectl exec -it postgresql-0 -n database -- psql $DATABASE_URL \
    -c "SELECT \"planId\", name, price, currency, \"isActive\", \"isDefault\" FROM cloud_plans;"
  3. Verify isActive is true and at least one plan has isDefault = true
  4. If no plans exist, run the bootstrap seed: kubectl exec -it deployment/kuploy -n kuploy -- npm run db:seed