Search the site
BACKEND
Sell subscriptions, charge with tax and promotions, and cap usage per customer, with every grant checked on the server.
Stripe, Paddle, Bachs and the store paths are tested against fakes. None has been run against the live service yet.
ON THIS PAGE
Sell a plan through Stripe, Paddle or Bachs
Sell in-app purchases, checked on your server
Charge with tax and promotions, and refund cleanly
Meter usage and cap it per tenant
Checkout reads the plan's price from the provider first and refuses when it differs from your plan, so a page cannot show one price and charge another.
Webhooks grant and revoke entitlements from subscription status, and each is believed only when its signature matches within five minutes.
Trials and usage meters are passed through to the provider.
Choose the provider in pubspec.yaml under dartvel.billing (provider: stripe, paddle or bachs). The block names the secrets that hold the keys, never the keys themselves.
Bachs charges cards in dollars or naira, and takes bank transfer and mobile money in local currencies. A sandbox key talks only to the Bachs sandbox, and a webhook is accepted while any of its current signing secrets matches, so rotating a secret drops nothing.
// What you sell, and what each plan turns on. The price is the number the
// application is held to: checkout reads the provider's own price first and
// refuses when the two disagree, so a page cannot show one price and charge
// another.
const BillingPlan team = BillingPlan(
id: 'team',
displayName: 'Team',
priceMinorUnits: 4900,
currency: 'USD',
trialDays: 14,
);
const Entitlement reports = Entitlement('reports');Copy code to clipboard
void useStripe() {
DV.Billing.useProvider(DVStripeBillingProvider(
secretKey: DV.Secrets.get('STRIPE_SECRET_KEY'),
webhookSecret: DV.Secrets.get('STRIPE_WEBHOOK_SECRET'),
// Your plan id on the left, the provider's price on the right.
prices: const <String, String>{'team': 'price_1234'},
// What each plan grants once the subscription is live.
entitlements: const <String, Set<Entitlement>>{
'team': <Entitlement>{reports},
},
successUrl: Uri.parse('https://example.com/billing/done'),
cancelUrl: Uri.parse('https://example.com/pricing'),
));
}Copy code to clipboard
Future<Uri?> subscribe(Workspace workspace) async {
final DVBillingCheckoutSession session =
await DV.Billing.checkout(plan: team, customer: workspace);
// Null when the provider gave back no hosted page. Send the customer
// nowhere; a blank tab is worse.
return session.checkoutUrl;
}Copy code to clipboard
// Asked on the server, never read off the client: the page that shows a
// feature and the code that allows it must not be two different answers.
Future<bool> mayExport(Workspace workspace) =>
DV.Billing.hasEntitlement(workspace, reports);Copy code to clipboard
Partial
Spec section: Billing
Planned work and implementation limits
No provider has been exercised against the live service.
Stripe, Paddle and Bachs keep webhook grants in memory, so a refund revokes a grant only with the local provider.
DV.Purchases.buy opens StoreKit 2 on iOS and macOS and Play Billing on Android, the gateway checkout on the web and desktop. A store that writes its own invoices, such as Telegram Stars, plugs in as a store adapter from a paid runtime package.
Subscriptions with trials and offers, non-consumables and consumables. Ask to Buy and slow payments come back as pending and finish later.
The device sends the receipt to your server, which asks the store. Only then is the purchase finished, and only what the server accepted.
DVPurchases writes a grant only from the store's own answer and refuses one bought under another account.
App Store Server Notifications v2 and Play Real-time Developer Notifications are signature-checked, applied once and in order. A refund or chargeback is told apart from a lapse.
Play: the service account lives in DV.Secrets (PLAY_SERVICE_ACCOUNT_JSON), the package name and the Pub/Sub push audience under dartvel.purchases.play, and a push is believed only with Google's signed token for that audience.
dartvel build android --format aab also writes the Play Data safety answers (data-safety.csv and a worksheet) beside the bundle. It fills in only what Dartvel itself sends and lists the rest as questions for you.
// What the application sells, declared once and read by both halves.
const DVPurchaseProduct proMonthly = DVPurchaseProduct(
'pro_monthly',
billable: DVBillable.digital(appStore: 'com.example.pro', play: 'pro'),
entitlements: <Entitlement>{Entitlement('pro')},
);
const DVPurchaseProduct coins = DVPurchaseProduct(
'coins_100',
billable: DVBillable.digital(
appStore: 'com.example.coins', play: 'coins',
telegram: 'coins', telegramStars: 50),
entitlements: <Entitlement>{},
kind: DVPurchaseKind.consumable,
);Copy code to clipboard
// In the app. The store sheet on iOS and Android, the gateway checkout on
// the web and desktop: one call, no platform branch.
DVPurchases.configure(DVPurchases.device(products: <DVPurchaseProduct>[
proMonthly,
coins,
]));
await DV.Purchases.refreshEntitlements();
final DVPurchaseOutcome outcome = await DV.Purchases.buy(proMonthly);
switch (outcome) {
case DVPurchaseCompleted(:final Set<String> consumed):
creditCoins(consumed);
case DVPurchasePending():
showWaitingForApproval();
case DVPurchaseCancelled() || DVPurchaseRedirected():
break;
case DVPurchaseFailed(:final String reason):
showError(reason);
}Copy code to clipboard
// Rebuilds when the server says what is held changed: a purchase, an Ask
// to Buy approved later, a refund at the next sync.
Widget build(BuildContext context) =>
DV.Purchases.watch(context, const Entitlement('pro'))
? const ProDashboard()
: const Upgrade();Copy code to clipboard
// On the server. pubspec.yaml declares `dartvel: purchases: {}`, which
// serves /_dartvel/purchases/verify, entitlements and the stores'
// notification endpoints.
DVPurchases.configure(DVPurchases(
products: <DVPurchaseProduct>[proMonthly, coins],
stores: <DVStoreAdapter>[
DVAppStoreAdapter(bundleId: 'com.example.app'),
// The service account comes from DV.Secrets (PLAY_SERVICE_ACCOUNT_JSON);
// the rest is `dartvel: purchases: play:` in pubspec.yaml.
DVPlayStoreAdapter.fromSecrets(const DVPlayStoreConfig(
packageName: 'com.example.app',
pushAudience:
'https://example.com/api/_dartvel/purchases/notifications/play',
pushServiceAccount: 'play-push@example.iam.gserviceaccount.com',
)),
],
ledger: DVDatabasePurchaseLedger(DV.Database.configuredAdapter!),
// Optional: what the web and desktop sell through.
gateway: stripe,
gatewayPlans: const <String, BillingPlan>{'pro_monthly': team},
));Copy code to clipboard
Partial
Spec section: Purchases and Entitlements
Planned work and implementation limits
The StoreKit 2 binding has not run on a Mac, and no purchase has gone through a real App Store or Play sandbox. The Play Billing binding has not run on an Android device, and the Play adapter has only met recorded Google responses.
Products are not generated from models.
DVTax asks your tax provider and refuses the sale when the answer does not add up. Nothing is ever taxed at zero by accident.
Promotions stack by group, respect global and per-customer limits under load, and are checked on the server.
DVCommerce.charge captures last, so a failure at any step leaves nothing behind. A refund reverses tax and revokes what the sale granted.
Partial
Spec section: Commerce: Tax, Promotions, Disputes and Payouts
Planned work and implementation limits
Tested against fakes only. No live tax, payment or payout provider has been run.
DVTaxTable ships no rates. It is the offline fallback and the reference for rounding.
Count or gauge anything per tenant, in memory or in your database. Each record needs an idempotency key, so a retry is not counted twice.
Limits can block, throttle or allow and bill, with thresholds announced once.
Each period's usage is reported to your billing provider, with a retry queue and a reconcile that is safe to run again.
Partial
Spec section: Usage Metering and Quotas
Planned work and implementation limits
No @DVMeter annotations, so meters are not generated or counted for you.
No dartvel meters commands, no Studio view, and no scheduled period close.
FSL-1.1-MIT licensed. Built with Dartvel.
Dartvel is made by
To the bottom