Access and grants
What a plan or product unlocks, and granting access by hand or from your server.
Access
This is the paywall. A plan or a product unlocks a collection when you choose it by name in that collection's Unlocked by row on its page in Data. Paying for the plan opens it; cancelling, lapsing or a full refund takes it back. The server refuses everyone else, and your app writes no check at all.
// On the Payments page: a plan named "pro" with a price
// In Data: "reports" → Unlocked by → tick "pro (plan)"
// Your app reads, catches entitlement_required and shows the upgrade prompt:
try {
const { records } = await g.collection("reports").list()
} catch (err) {
if (err.code === "entitlement_required") showUpgrade(err.requires) // "access:pro", the plan's access key (see below)
}
Call plans and products whatever your business calls them (pro, Film X, Course: Foundations). Gemmein attaches no meaning to the names, so a new tier needs no Gemmein release. A collection can be unlocked by several plans or products at once ("anyone on pro or studio, or who bought Midnight Pack"), and any one of them opens it. A person can hold access for several reasons at once, such as a plan, a lifetime purchase and a manual grant from the dashboard, and it ends only when the last one does.
Access keys. Each plan and product carries one access key, generated
from the name when it is first saved: plan "pro" → access:pro, product
"Midnight Pack" → access:midnight-pack. A rename keeps the key, so nobody loses
access. It is what err.requires carries (one key, a string; if several plans
open the collection, it names the first one) and what g.purchases.mine() lists
under grants. The dashboard shows it only as a muted hint beside the name, and
nobody types it.
Access is set per collection. "Three free lessons, the rest
premium" means two collections, one open and one locked. "Must hold both" cannot be
expressed; that is a second collection. A locked public_read or
community collection becomes authenticated access: an anonymous reader
is told to sign in (never which plan they lack).
What your app can see. It reads a person's purchases and what each
one granted (g.purchases.mine()) and their subscription
(g.subscriptions.mine()). By-hand grants are not listed to the app, and a
locked read succeeds. So never rebuild the paywall client-side from the lists you can see;
let the server refuse, and show the upgrade prompt on entitlement_required. To
give someone access, grant it on their page in the dashboard; it is never code.
Grants
Every bit of access a person holds is a grant: one access key, one reason, a start and maybe an end. Grants have seven sources in three families. The family decides who can create a grant, what ends it, and whether it counts as revenue on your dashboard.
| Family | Sources | Who writes it | Ends when | Counts as revenue |
|---|---|---|---|---|
| Purchase-tied | subscription, purchase | Only a payment: Stripe's signed webhook, or the relay action (fulfil_product, grant_plan) set as how the product or plan is sold. A bank debit counts once it clears. Never you by hand, never your app | A subscription grant: the subscription cancels, lapses or is revoked by its relay. A purchase grant: a full refund (a partial refund leaves it) | Yes, the only grants that do |
| By hand | by hand, trial, promotion, migration (people you brought over from somewhere else) | You, from a person's page, or your server with a secret key (grantAccess), choosing the plan or product by name, with an optional end date and a reason | On its end date, or when you revoke it | No |
| By relay | relay | A relay, when its trigger fires: a provider's signed webhook, a schedule, or a record change. No secret key can create one | On the date the action named, or when you revoke it | No |
- A grant is never edited. An extension is a new grant, so the reason on every grant stays true.
- You can end a purchase-tied grant by hand from the person's page. That stops the access and refunds nothing; money moves only in Stripe.
- A refund never changes a by-hand or relay grant, and revoking a by-hand grant never triggers a refund.
- A relay grant reads "granted by relay <name>" on the person's page.
Which grant to use:
- "Give them a free month": a trial grant with an end date.
- "Launch deal, first 50 get pro": a promotion grant.
- "Support fix, they were double-charged": a by hand grant with the reason written down.
- "They paid on my old platform": a migration grant, so the record says why.
- "They paid through GoCardless": a relay grant, written when the provider's signed webhook lands.
- "They paid": never by hand, because Stripe writes it, so revenue reflects only money that arrived.
Thanks for telling us.
Thanks for telling us.
Last updated