Plans and subscriptions
Charge for your app without writing any payment code. You define plans, your app asks the SDK who has access, and dg.run does the paying: from the user's wallet or by card with Stripe. Each plan is priced in US dollars or Iranian rials.
The plan types
| Type | What the user gets | Price |
|---|---|---|
| Free trial | Access for N days (1 to 90). Once per user per app. | Free |
| Subscription | Access for one period (every N weeks, months or years). Renews by itself from the user's wallet until they stop it. | $1 to $1,000, or 100,000 to 5,000,000,000 rials |
| One-time purchase | Access for N days, or for life (0 days). Never renews. | $1 to $1,000, or 100,000 to 5,000,000,000 rials |
Create plans in My apps → Plans. Each has a key (for example monthly) that your app uses to refer to it, a name, a short description and a list of features. You can have up to 10 plans per app.
A plan's type, price and length can't be changed after it exists, because subscribers agreed to them. To change a price, create a new plan and archive the old one: it disappears for new buyers and current subscribers keep what they bought. You can delete only a plan nobody has ever taken.
Use it in your app
import { createDg } from '@dgrun/sdk'
const dg = createDg()
const plans = await dg.billing.plans() // what you offer (no sign-in needed)
const sub = await dg.billing.status() // null, or the signed-in user's subscription
if (sub?.active) { /* unlock the paid features */ }
await dg.billing.subscribe('monthly') // sends the user to dg.run to pay, then back to this page
await dg.billing.cancel() // stop auto-renew (access lasts until the period ends)
status() returns { active, plan, startedAt, currentPeriodEnd, autoRenew, trial, renewalError }. currentPeriodEnd is null for lifetime access. An ended trial or subscription is still returned, with active: false, so you can say "your trial ended".
When the user comes back from paying, the SDK cleans the address bar and resolves dg.billing.checkout with { result: 'ok' | 'cancel', plan } (or null on any other page load). It doesn't tell you whether they paid: call status().
The Pro Notes example in the repository (examples/pro-notes) is a complete app: a free tier, pricing cards built from plans(), a "Your plan" panel, and gated features.
How paying works
- Your app calls
subscribe('monthly'). The browser goes to dg.run/subscribe, which shows your app, the plan and the price, and the user's wallet balance. - The user confirms on dg.run, with their own session, and chooses to pay from the wallet or by card (Stripe). A free trial needs one click.
- The browser comes back to your app, which calls
status().
Your page never sees a card and never moves money. An app can only read plans and status, cancel auto-renew and ask the user to go and pay; it can't charge anyone.
Wallet and renewals
Every user has a wallet in their Profile: they add money by card, and it pays for plans in any app and renews subscriptions. Each payment and top-up appears in their transaction history.
- A subscription renews automatically from the wallet when its period ends. If the wallet is short, the renewal fails and the subscription expires (
renewalErroris set toinsufficient_funds); the user can top up and subscribe again. - Paying a subscription by card buys one period; later renewals come from the wallet.
- Switching to another plan replaces the current one immediately. The time left on the old one isn't refunded.
- The server decides: renewals and expiry are applied whenever someone reads a subscription, and a daily job sweeps the rest.
In My apps → Data and users → Users you can see each user's plan, how much time they have left (or Lifetime) and whether it auto-renews.
Money and testing
- Customers pay dg.run. Your Plans page shows what customers paid for each plan and what you receive after tax, fees and the platform share; the platform pays you out by hand (see above).
- In an admin preview only free trials can be tested (paid plans are disabled so previews can't spend real money).
- With no Stripe key configured, card payments are simulated in development: nothing is charged and the flow works end to end.
Checking access offline (signed proof)
status() always asks the server. For an app that must also work offline, use entitlement():
const e = await dg.billing.entitlement() // null, or { plan, trial, periodEnd, validUntil, source }
if (e) { /* unlock */ }
While a subscription is active the server gives the SDK a signed proof (Ed25519). The SDK saves it in IndexedDB and verifies it on the device with dg.run's public key, so it works offline for up to 15 days (never past the subscription's end), and editing the saved data breaks the signature. It is bound to your app and that user. Online, the SDK replaces a proof older than a day (change it with createDg({ proofRefreshAfterMs })) and drops it as soon as the subscription is gone, the user signs out or disconnects the app. To trust only a key you hold, copy it from https://<dg.run>/api/jwks and pass createDg({ publicKeys }). Browsers without Ed25519 in WebCrypto get null offline (fail closed).
Currencies
Every plan is priced in one currency, USD or IRR (Iranian rial, no decimals), and you choose it when you create the plan. It can't be changed once someone has used the plan. Nothing is ever converted: a rial plan is paid from the customer's rial balance, a dollar plan from their dollar balance. Both live in the same wallet. Cards (Stripe) work for dollars only; rial balances are added by the platform. An app can offer plans in both currencies.
What you earn and how you get paid
Customers pay the full plan price. Your income is what is left after the sales tax included in the price, card processing costs and the platform's share. The rates are shown on your plans page, with a chart of your income and, for each plan, how every sale is split. When you set a price you see roughly what you will receive per sale. Rates can change, but a sale always keeps the split it was made with.
To be paid, add a payout method on your Payouts page (Developer section) for each currency you sell in: PayPal, Interac e-Transfer, bank transfer or another way for dollars; your SHEBA number, account owner and bank name for rials. The platform decides which options are available for each currency. When your balance reaches the minimum, the platform sends the money and records the payout, and you see it on the same page.
Things to know
status().activeis computed by the server every time. What your page does with it is only as strong as your page: static code runs in the user's browser and a determined user can edit it. Use it to decide what to show, and gate anything that must be protected on a server you control.- Errors are
DgErrors:plan_not_found(no such plan, or archived),not_recurring(only subscriptions can be cancelled),unauthorized(sign in first). - Deleting your app removes its plans and subscriptions. Users' payment history stays.
dg.run