
Provider-agnostic, self-hosted billing syncing payment events to Supabase as single source-of-truth; multi-provider plugins, CLI/AI setup, built-in paywall UI, dashboard and multi-tenant isolation.
Craft your own billing. Any provider. Any platform. 15 minutes.
Self-hosted, provider-agnostic billing library for Kotlin Multiplatform apps. Uses Supabase as the source of truth — your app never talks to a payment provider directly. Open-source SDK + optional hosted dashboard for subscriber management and analytics.
Client App ──→ PayCraft SDK ──→ Supabase (source of truth)
↑
Payment Provider ──webhook──────┘
↑
Dashboard ──────────────────────┘
npx paycraft initThe CLI scaffolds your Supabase schema, Edge Functions, and app configuration interactively.
The fastest way to add PayCraft billing to your KMP app — no cloning, no setup, one prompt.
cd /path/to/your-kmp-app
claudeFetch https://raw.githubusercontent.com/mobilebytelabs/paycraft/main/client-skills/paycraft-adopt.md
Save it to .claude/commands/paycraft-adopt.md in this project, then run /paycraft-adopt.
That's it. Claude handles everything from here:
| Phase | What Claude does | What you do |
|---|---|---|
| Bootstrap | Searches for PayCraft locally, offers to clone it if not found — asks where on your system | Pick a location (or press Enter for ~/paycraft/) |
| Phase 1 — ENV | Creates .env, walks you through each credential |
Answer ~8 questions (Supabase keys, Stripe key, plans) |
| Phase 2 — Supabase | Applies migrations, creates RPCs, deploys webhook, verifies every step | Nothing |
| Phase 3 — Stripe | Creates test product, prices, payment links automatically via Stripe MCP | 2 browser steps: add webhook endpoint + enable portal |
| Phase 4 — Client | Adds dependency, writes PayCraft.configure(), wires Koin + paywall UI |
Nothing |
| Phase 5 — Verify | Writes real DB row, calls is_premium(), confirms true, cleans up |
Nothing |
Result: fully operational billing in test mode. No real charges until you opt in.
After setup, these commands are available in your project:
/paycraft-adopt-env ← re-collect credentials (key rotation)
/paycraft-adopt-supabase ← re-deploy webhook or re-apply migrations
/paycraft-adopt-stripe ← create live products when ready to ship
/paycraft-adopt-client ← re-integrate into a different app
/paycraft-adopt-verify ← re-verify after any config change
/paycraft-adopt-migrate ← migrate to new Supabase/Stripe account or switch providers
After setup completes, your app contains a .paycraft/ directory — a complete record of
how PayCraft is configured in your project:
your-app/
├── .env ← gitignored (auto-added by setup) — all PayCraft keys
└── .paycraft/
├── config.json ← setup answers — safe to commit
├── deployment.json ← resource IDs, no secrets — safe to commit
├── supabase/ ← SQL + Edge Function backup — safe to commit
└── backups/ ← gitignored — timestamped .env copies
Step 5 automatically adds .env and .paycraft/backups/ to your .gitignore.
Everything else in .paycraft/ is safe to commit as deployment documentation.
npm install -g @anthropic-ai/claude-code)Moving to a new Supabase org, rotating Stripe accounts, or switching from Stripe to Razorpay?
/paycraft-adopt-migrate
Claude shows your current deployment, asks what's changing, backs up your .env, collects
only the new credentials, re-deploys only the affected components, and re-verifies end-to-end.
No full re-setup. Subscriber data migration included (optional).
| Migration | Command handles |
|---|---|
| New Supabase project | Re-deploys migrations + webhook, migrates subscriber data |
| New Stripe account | Re-creates products, prices, payment links, webhook |
| New Razorpay account | Re-creates plans, payment links, webhook |
| Stripe → Razorpay | Full provider switch, updates app config |
| Razorpay → Stripe | Full provider switch, updates app config |
PayCraftSheet, PayCraftBanner, PayCraftRestore out of the boxnpx paycraft init scaffolds everything in minutesPayCraftModule, get BillingManager everywhereIf you prefer to set up without Claude AI, see docs/QUICK_START.md.
# gradle/libs.versions.toml
[versions]
paycraft = "LATEST_VERSION" # check Maven Central badge above
[libraries]
paycraft = { module = "io.github.mobilebytelabs:paycraft", version.ref = "paycraft" }// shared/build.gradle.kts — commonMain ONLY
commonMain.dependencies {
implementation(libs.paycraft)
}PayCraft.configure {
supabase(
url = BuildConfig.SUPABASE_URL,
anonKey = BuildConfig.SUPABASE_ANON_KEY,
)
provider(
StripeProvider(
paymentLinks = mapOf(
"monthly" to BuildConfig.STRIPE_MONTHLY_LINK,
"quarterly" to BuildConfig.STRIPE_QUARTERLY_LINK,
"yearly" to BuildConfig.STRIPE_YEARLY_LINK,
),
customerPortalUrl = BuildConfig.STRIPE_PORTAL_URL,
)
)
plans(
BillingPlan(id = "monthly", name = "Monthly", price = "₹99", interval = "/month"),
BillingPlan(id = "quarterly", name = "Quarterly", price = "₹249", interval = "/3 months"),
BillingPlan(id = "yearly", name = "Yearly", price = "₹799", interval = "/year", isPopular = true),
)
benefits(
BillingBenefit(icon = Icons.Default.Block, text = "Ad-free experience"),
BillingBenefit(icon = Icons.Default.Download, text = "Unlimited downloads"),
)
supportEmail("support@yourdomain.com")
}startKoin {
modules(yourModules, PayCraftModule)
}var showPaywall by remember { mutableStateOf(false) }
var showRestore by remember { mutableStateOf(false) }
PayCraftBanner(
onClick = { showPaywall = true },
onRestoreClick = { showRestore = true },
)
PayCraftSheet(visible = showPaywall, onDismiss = { showPaywall = false })
PayCraftRestore(visible = showRestore, onDismiss = { showRestore = false })val isPremium by billingManager.isPremium.collectAsState()
if (isPremium) PremiumContent() else FreeContent()| Provider | Status | Checkout | Webhook |
|---|---|---|---|
| Stripe | Stable | Payment Links | stripe-webhook |
| Razorpay | Stable | Payment Links | razorpay-webhook |
| Paddle | Ready | Overlay Checkout | paddle-webhook |
| PayPal | Ready | Smart Buttons | paypal-webhook |
| LemonSqueezy | Ready | Hosted Checkout | lemonsqueezy-webhook |
| Flutterwave | Ready | Standard Checkout | flutterwave-webhook |
| Paystack | Ready | Popup Checkout | paystack-webhook |
| Midtrans | Ready | Snap | midtrans-webhook |
| BTCPay | Ready | Invoice | btcpay-webhook |
| Custom | Stable | Any URL | Implement PaymentProvider
|
| Component | Description |
|---|---|
PayCraftSheet |
Conditional bottom-sheet paywall |
PayCraftPaywall |
Full-screen paywall |
PayCraftBanner |
Settings row — upgrade CTA or premium status |
PayCraftRestore |
Email-based restore purchases dialog |
PayCraftPremiumGuard |
Gate any composable behind premium |
| Platform | Targets |
|---|---|
| Android | android |
| iOS |
iosX64, iosArm64, iosSimulatorArm64
|
| macOS |
macosX64, macosArm64
|
| JVM | jvm |
| JavaScript |
js (Browser, Node.js) |
| WebAssembly |
wasmJs (Browser) |
The PayCraft web dashboard provides tenant admins with:
Live: dashboard-chi-ashen-99.vercel.app
PayCraft uses a multi-tenant architecture where each app gets isolated data within a shared Supabase deployment:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ App A │ │ App B │ │ App C │
│ (SDK) │ │ (SDK) │ │ (SDK) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ X-PayCraft-API-Key header │
└───────────┬───────┴───────────────────┘
▼
┌────────────────┐
│ Supabase │
│ (RPCs + RLS) │◄──── Webhooks (per-tenant URLs)
└────────┬───────┘
▼
┌────────────────┐
│ Dashboard │
│ (Next.js) │
└────────────────┘
Self-hosted users: omit the API key — RPCs default to single-tenant mode.
See paycraft-sample/ for minimal per-platform projects:
android-compose/ — Android Compose + Koinios-swiftui/ — SwiftUI wrapperdesktop-jvm/ — Desktop Composeweb-wasm/ — Browser WasmJsCreate custom provider plugins as separate Maven artifacts:
class MyGatewayPlugin : ProviderPlugin {
override val providerId = "my-gateway"
override fun createProvider(config: PluginConfig): PaymentProvider { ... }
}See plugin-template/ for a starter template.
./gradlew jvmTest # run tests
./gradlew :sample-app:run # desktop sample
./gradlew spotlessApply # format
./gradlew detekt # lint./scripts/release.sh
# or: /lib-release in Claude Code| Service | URL |
|---|---|
| Documentation | mobilebytelabs.github.io/PayCraft |
| Dashboard | dashboard-chi-ashen-99.vercel.app |
| CLI | npmjs.com/package/paycraft |
| SDK | Maven Central |
See CONTRIBUTING.md.
Copyright 2026 MobileByteLabs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Craft your own billing. Any provider. Any platform. 15 minutes.
Self-hosted, provider-agnostic billing library for Kotlin Multiplatform apps. Uses Supabase as the source of truth — your app never talks to a payment provider directly. Open-source SDK + optional hosted dashboard for subscriber management and analytics.
Client App ──→ PayCraft SDK ──→ Supabase (source of truth)
↑
Payment Provider ──webhook──────┘
↑
Dashboard ──────────────────────┘
npx paycraft initThe CLI scaffolds your Supabase schema, Edge Functions, and app configuration interactively.
The fastest way to add PayCraft billing to your KMP app — no cloning, no setup, one prompt.
cd /path/to/your-kmp-app
claudeFetch https://raw.githubusercontent.com/mobilebytelabs/paycraft/main/client-skills/paycraft-adopt.md
Save it to .claude/commands/paycraft-adopt.md in this project, then run /paycraft-adopt.
That's it. Claude handles everything from here:
| Phase | What Claude does | What you do |
|---|---|---|
| Bootstrap | Searches for PayCraft locally, offers to clone it if not found — asks where on your system | Pick a location (or press Enter for ~/paycraft/) |
| Phase 1 — ENV | Creates .env, walks you through each credential |
Answer ~8 questions (Supabase keys, Stripe key, plans) |
| Phase 2 — Supabase | Applies migrations, creates RPCs, deploys webhook, verifies every step | Nothing |
| Phase 3 — Stripe | Creates test product, prices, payment links automatically via Stripe MCP | 2 browser steps: add webhook endpoint + enable portal |
| Phase 4 — Client | Adds dependency, writes PayCraft.configure(), wires Koin + paywall UI |
Nothing |
| Phase 5 — Verify | Writes real DB row, calls is_premium(), confirms true, cleans up |
Nothing |
Result: fully operational billing in test mode. No real charges until you opt in.
After setup, these commands are available in your project:
/paycraft-adopt-env ← re-collect credentials (key rotation)
/paycraft-adopt-supabase ← re-deploy webhook or re-apply migrations
/paycraft-adopt-stripe ← create live products when ready to ship
/paycraft-adopt-client ← re-integrate into a different app
/paycraft-adopt-verify ← re-verify after any config change
/paycraft-adopt-migrate ← migrate to new Supabase/Stripe account or switch providers
After setup completes, your app contains a .paycraft/ directory — a complete record of
how PayCraft is configured in your project:
your-app/
├── .env ← gitignored (auto-added by setup) — all PayCraft keys
└── .paycraft/
├── config.json ← setup answers — safe to commit
├── deployment.json ← resource IDs, no secrets — safe to commit
├── supabase/ ← SQL + Edge Function backup — safe to commit
└── backups/ ← gitignored — timestamped .env copies
Step 5 automatically adds .env and .paycraft/backups/ to your .gitignore.
Everything else in .paycraft/ is safe to commit as deployment documentation.
npm install -g @anthropic-ai/claude-code)Moving to a new Supabase org, rotating Stripe accounts, or switching from Stripe to Razorpay?
/paycraft-adopt-migrate
Claude shows your current deployment, asks what's changing, backs up your .env, collects
only the new credentials, re-deploys only the affected components, and re-verifies end-to-end.
No full re-setup. Subscriber data migration included (optional).
| Migration | Command handles |
|---|---|
| New Supabase project | Re-deploys migrations + webhook, migrates subscriber data |
| New Stripe account | Re-creates products, prices, payment links, webhook |
| New Razorpay account | Re-creates plans, payment links, webhook |
| Stripe → Razorpay | Full provider switch, updates app config |
| Razorpay → Stripe | Full provider switch, updates app config |
PayCraftSheet, PayCraftBanner, PayCraftRestore out of the boxnpx paycraft init scaffolds everything in minutesPayCraftModule, get BillingManager everywhereIf you prefer to set up without Claude AI, see docs/QUICK_START.md.
# gradle/libs.versions.toml
[versions]
paycraft = "LATEST_VERSION" # check Maven Central badge above
[libraries]
paycraft = { module = "io.github.mobilebytelabs:paycraft", version.ref = "paycraft" }// shared/build.gradle.kts — commonMain ONLY
commonMain.dependencies {
implementation(libs.paycraft)
}PayCraft.configure {
supabase(
url = BuildConfig.SUPABASE_URL,
anonKey = BuildConfig.SUPABASE_ANON_KEY,
)
provider(
StripeProvider(
paymentLinks = mapOf(
"monthly" to BuildConfig.STRIPE_MONTHLY_LINK,
"quarterly" to BuildConfig.STRIPE_QUARTERLY_LINK,
"yearly" to BuildConfig.STRIPE_YEARLY_LINK,
),
customerPortalUrl = BuildConfig.STRIPE_PORTAL_URL,
)
)
plans(
BillingPlan(id = "monthly", name = "Monthly", price = "₹99", interval = "/month"),
BillingPlan(id = "quarterly", name = "Quarterly", price = "₹249", interval = "/3 months"),
BillingPlan(id = "yearly", name = "Yearly", price = "₹799", interval = "/year", isPopular = true),
)
benefits(
BillingBenefit(icon = Icons.Default.Block, text = "Ad-free experience"),
BillingBenefit(icon = Icons.Default.Download, text = "Unlimited downloads"),
)
supportEmail("support@yourdomain.com")
}startKoin {
modules(yourModules, PayCraftModule)
}var showPaywall by remember { mutableStateOf(false) }
var showRestore by remember { mutableStateOf(false) }
PayCraftBanner(
onClick = { showPaywall = true },
onRestoreClick = { showRestore = true },
)
PayCraftSheet(visible = showPaywall, onDismiss = { showPaywall = false })
PayCraftRestore(visible = showRestore, onDismiss = { showRestore = false })val isPremium by billingManager.isPremium.collectAsState()
if (isPremium) PremiumContent() else FreeContent()| Provider | Status | Checkout | Webhook |
|---|---|---|---|
| Stripe | Stable | Payment Links | stripe-webhook |
| Razorpay | Stable | Payment Links | razorpay-webhook |
| Paddle | Ready | Overlay Checkout | paddle-webhook |
| PayPal | Ready | Smart Buttons | paypal-webhook |
| LemonSqueezy | Ready | Hosted Checkout | lemonsqueezy-webhook |
| Flutterwave | Ready | Standard Checkout | flutterwave-webhook |
| Paystack | Ready | Popup Checkout | paystack-webhook |
| Midtrans | Ready | Snap | midtrans-webhook |
| BTCPay | Ready | Invoice | btcpay-webhook |
| Custom | Stable | Any URL | Implement PaymentProvider
|
| Component | Description |
|---|---|
PayCraftSheet |
Conditional bottom-sheet paywall |
PayCraftPaywall |
Full-screen paywall |
PayCraftBanner |
Settings row — upgrade CTA or premium status |
PayCraftRestore |
Email-based restore purchases dialog |
PayCraftPremiumGuard |
Gate any composable behind premium |
| Platform | Targets |
|---|---|
| Android | android |
| iOS |
iosX64, iosArm64, iosSimulatorArm64
|
| macOS |
macosX64, macosArm64
|
| JVM | jvm |
| JavaScript |
js (Browser, Node.js) |
| WebAssembly |
wasmJs (Browser) |
The PayCraft web dashboard provides tenant admins with:
Live: dashboard-chi-ashen-99.vercel.app
PayCraft uses a multi-tenant architecture where each app gets isolated data within a shared Supabase deployment:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ App A │ │ App B │ │ App C │
│ (SDK) │ │ (SDK) │ │ (SDK) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ X-PayCraft-API-Key header │
└───────────┬───────┴───────────────────┘
▼
┌────────────────┐
│ Supabase │
│ (RPCs + RLS) │◄──── Webhooks (per-tenant URLs)
└────────┬───────┘
▼
┌────────────────┐
│ Dashboard │
│ (Next.js) │
└────────────────┘
Self-hosted users: omit the API key — RPCs default to single-tenant mode.
See paycraft-sample/ for minimal per-platform projects:
android-compose/ — Android Compose + Koinios-swiftui/ — SwiftUI wrapperdesktop-jvm/ — Desktop Composeweb-wasm/ — Browser WasmJsCreate custom provider plugins as separate Maven artifacts:
class MyGatewayPlugin : ProviderPlugin {
override val providerId = "my-gateway"
override fun createProvider(config: PluginConfig): PaymentProvider { ... }
}See plugin-template/ for a starter template.
./gradlew jvmTest # run tests
./gradlew :sample-app:run # desktop sample
./gradlew spotlessApply # format
./gradlew detekt # lint./scripts/release.sh
# or: /lib-release in Claude Code| Service | URL |
|---|---|
| Documentation | mobilebytelabs.github.io/PayCraft |
| Dashboard | dashboard-chi-ashen-99.vercel.app |
| CLI | npmjs.com/package/paycraft |
| SDK | Maven Central |
See CONTRIBUTING.md.
Copyright 2026 MobileByteLabs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.