Dartvel, home
Docs
Features
Studio
Cloud
Compared

Search the site

GitHub
pub.dev

GETTING STARTED

Getting started
Existing Flutter apps
Existing Native apps
Run on your phone

APP

UI and styling
Routing
State
Accessibility
Keyboard shortcuts
Localization
Devices and desktop
Native device access
Media, 3D and XR

DATA

Data models
Forms
Search
Sync and offline
Import and export
Change capture
Database
Cache
File storage
Images
Privacy and erasure

BACKEND

Backend functions
Auth and sessions
Authorization
Queues and jobs
Workers and memory
Notifications and mail
Outbound HTTP
AI
Webhooks
GraphQL and OpenAPI
API keys and OAuth
Multi-tenancy
Billing and commerce
Modules

OPERATIONS

Edge security
Secrets and environments
Monitoring
Releases

SHIPPING

Build targets
Static web hosting
Servers and deploying

REFERENCE

Testing
CLI reference
Coding agents

BACKEND

Billing and commerce

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

Sell a plan through Stripe, Paddle or Bachs

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.

Sell in-app purchases, checked on your server

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.

Charge with tax and promotions, and refund cleanly

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.

Meter usage and cap it per tenant

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.

PREVIOUS Multi-tenancy One deployment, many customers
NEXT Modules Mount apps, and wrap Dart, npm, C, Rust, JVM or Swift code
GitHub
pub.dev
npm
Acknowledgements
Privacy
Terms

FSL-1.1-MIT licensed. Built with Dartvel.

Dartvel is made by

SigmaDev Digital

To the bottom