[Design] Hosted Quest Board: GM plans with campaign slots, AI usage pricing, and an entitlements layer #642

Open
opened 2026-09-14 03:25:00 +00:00 by claude-bot · 5 comments
Collaborator

Design record for running Quest Board as a paid hosted service, written 2026-09-13 from a discussion with the owner. It records decisions, the architecture they imply, and the questions still open. It builds nothing itself. The first buildable pieces are #643 (campaign invites) and #644 (campaign ownership), both worth doing for self-hosters regardless.

Decisions

  1. Billing unit: a GM account plan with campaign slots. A plan holds slots; each active campaign occupies one slot on its owner's plan. Pricing is unset, but the working figure is roughly $3 per month per campaign slot for the non-AI features, intended to cover server costs.
  2. There is no free tier. Accounts are free and players never pay just to play. Owning an active campaign needs a slot.
  3. AI features are priced separately, by usage. Either tiers of recorded hours or a credit system; not yet decided. Transcription and summarisation are nearly all of the marginal cost, and they scale with recorded hours, not with campaigns.
  4. Join approval is a per-campaign GM setting (#643).
  5. Existing campaign invite codes get a 7-day expiry when invites gain expiry and usage limits (#643).
  6. A campaign has an owner, separate from its GMs (#644). The owner may be a player: for example, someone paying for their group's campaign. Only the owner sets GMs, archives and un-archives, trashes the campaign, and transfers ownership. Ownership grants no GM content access.
  7. Self-hosting stays first-class (assumed, consistent with v4.2.0's direction). On a self-hosted instance every entitlement is unlimited, and INVITE_CODE remains that instance's registration gate.

What changes between self-hosted and hosted

On a self-hosted instance the gate is who may create an account (INVITE_CODE). A hosted service has open sign-up; the gate moves to who may own an active campaign (a slot) and how much AI they may use (an allowance).

Invites then stop being a registration control and protect two other things:

  • Privacy. Joining a campaign opens transcripts and summaries of real people's speech.
  • Membership. Who is at the table at all.

That is why invites get expiry, usage limits and optional GM approval regardless of billing.

Architecture this implies

An entitlements interface; core code never talks to a payment provider.

  • entitlement_service answers questions such as can_own_active_campaign(user), campaign_is_active(campaign) and ai_allowance(campaign or account).
  • Self-hosted provider: returns unlimited and active. This is the default, and the whole billing layer is inert without configuration.
  • Hosted provider: reads plan state from local tables kept in sync by the payment provider's webhooks. Request handling never makes a live call to the provider.

The owner is the billing anchor (campaigns.owner_id, #644).

  • Always a member, as player or GM.
  • Transfer requires the recipient to accept; on a hosted instance, accepting needs a free slot.
  • A campaign may have no GM; the owner assigns one.
  • Account deletion blocks on owned campaigns.

Enforcement points:

Where Check
Campaign create, un-archive, accepting an ownership transfer the owner-to-be has a free slot
Invite redemption campaign active (and a member cap, if plans ever carry one)
/record start (bot → backend) campaign active and AI allowance remaining. This is a new bot API check: a contract-version bump if a refusal is a shape an older bot would mishandle.
process_audio, the LLM fan-out and on-demand generators allowance, metered against session_usage (#357), which already records audio submitted and transcribed and LLM usage per session

Lapse and downgrade:

  • Lapse: the campaign becomes read-only, not deleted. Archive-style (is_archived exists), with a grace period and export available (campaign export exists, #580).
  • Audio retention and trash deletion must not destroy a lapsed campaign's recordings before that grace period ends. The two clocks have to agree rather than race.
  • Downgrade from N slots to M: the owner picks which campaigns stay active; the rest go read-only. Nothing is chosen silently.

BYO-AI (v4.2.0) is a natural pricing lever. A campaign using its own provider key consumes no host AI capacity.

Open questions

  • Pricing: the slot price, the AI unit (hours vs credits), whether allowances pool across an owner's campaigns or sit per campaign, and whether unused allowance rolls over.
  • Whose AI allowance a campaign draws on: the owner's plan, presumably, since they own the slot.
  • Do BYO-AI campaigns draw from the allowance at all (host ASR with BYO LLM is a mixed case)?
  • A trial: can a new owner try a campaign before paying? "No free tier" rules out free forever, not necessarily a trial.
  • Can someone with no slot still create a draft campaign and invite players before paying?
  • Member caps per plan: needed at all, given tables are 3–7 people?
  • Payment provider. Stripe is the usual choice. A merchant of record (Paddle, Lemon Squeezy) handles international VAT and sales tax, which matters for a solo operator selling abroad.
  • Should one hosted Discord bot serve every guild? Today one bot serves one instance.
  • Hosting prerequisites that aren't billing: terms of service, privacy policy, and data-processing terms for voice recordings. The standing-consent model in CLAUDE.md assumes groups who know each other; with strangers able to join a hosted table, the join step is where standing consent would need to be stated. To be decided, not a per-session consent gate.

Phasing

  1. #643, campaign invites with expiry, usage limits and per-campaign approval, including the INVITE_CODE gate fix. #644, campaign ownership. Independent of each other; both useful to self-hosters now.
  2. Entitlements interface with the self-hosted "unlimited" provider, and the enforcement points wired but inert. No payments.
  3. Hosted provider: plan tables, payment-provider webhooks, lapse and downgrade handling.
  4. AI metering and allowance enforcement, once the pricing model is chosen.
  • #534: the invite link vs INVITE_CODE investigation that started this.
  • #357: per-session cost telemetry, the metering foundation.
  • v4.2.0: BYO-AI and provider abstraction.
  • #415: bot event stream sized for hosted scale.

Revised 2026-09-13: the draft's slot_owner_id (a billing-only field) was replaced by campaign ownership (#644), after the owner pointed out that the payer may be a player rather than a GM.

Design record for running Quest Board as a paid hosted service, written 2026-09-13 from a discussion with the owner. It records **decisions**, the **architecture they imply**, and the **questions still open**. It builds nothing itself. The first buildable pieces are **#643** (campaign invites) and **#644** (campaign ownership), both worth doing for self-hosters regardless. ## Decisions 1. **Billing unit: a GM account plan with campaign slots.** A plan holds slots; each active campaign occupies one slot on its **owner's** plan. Pricing is unset, but the working figure is roughly **$3 per month per campaign slot** for the non-AI features, intended to cover server costs. 2. **There is no free tier.** Accounts are free and players never pay just to play. *Owning* an active campaign needs a slot. 3. **AI features are priced separately, by usage.** Either tiers of recorded hours or a credit system; not yet decided. Transcription and summarisation are nearly all of the marginal cost, and they scale with recorded hours, not with campaigns. 4. **Join approval is a per-campaign GM setting** (#643). 5. **Existing campaign invite codes get a 7-day expiry** when invites gain expiry and usage limits (#643). 6. **A campaign has an owner, separate from its GMs** (#644). The owner may be a player: for example, someone paying for their group's campaign. Only the owner sets GMs, archives and un-archives, trashes the campaign, and transfers ownership. Ownership grants no GM content access. 7. **Self-hosting stays first-class** (assumed, consistent with v4.2.0's direction). On a self-hosted instance every entitlement is unlimited, and `INVITE_CODE` remains that instance's registration gate. ## What changes between self-hosted and hosted On a self-hosted instance the gate is **who may create an account** (`INVITE_CODE`). A hosted service has open sign-up; the gate moves to **who may own an active campaign** (a slot) and **how much AI they may use** (an allowance). Invites then stop being a registration control and protect two other things: - **Privacy.** Joining a campaign opens transcripts and summaries of real people's speech. - **Membership.** Who is at the table at all. That is why invites get expiry, usage limits and optional GM approval regardless of billing. ## Architecture this implies **An entitlements interface; core code never talks to a payment provider.** - `entitlement_service` answers questions such as `can_own_active_campaign(user)`, `campaign_is_active(campaign)` and `ai_allowance(campaign or account)`. - **Self-hosted provider:** returns unlimited and active. This is the default, and the whole billing layer is inert without configuration. - **Hosted provider:** reads plan state from local tables kept in sync by the payment provider's webhooks. Request handling never makes a live call to the provider. **The owner is the billing anchor (`campaigns.owner_id`, #644).** - Always a member, as player or GM. - Transfer requires the recipient to accept; on a hosted instance, accepting needs a free slot. - A campaign may have no GM; the owner assigns one. - Account deletion blocks on owned campaigns. **Enforcement points:** | Where | Check | |---|---| | Campaign create, un-archive, accepting an ownership transfer | the owner-to-be has a free slot | | Invite redemption | campaign active (and a member cap, *if* plans ever carry one) | | `/record start` (bot → backend) | campaign active and AI allowance remaining. This is a new bot API check: a contract-version bump if a refusal is a shape an older bot would mishandle. | | `process_audio`, the LLM fan-out and on-demand generators | allowance, metered against `session_usage` (#357), which already records audio submitted and transcribed and LLM usage per session | **Lapse and downgrade:** - **Lapse:** the campaign becomes **read-only, not deleted**. Archive-style (`is_archived` exists), with a grace period and export available (campaign export exists, #580). - Audio retention and trash deletion must not destroy a lapsed campaign's recordings before that grace period ends. The two clocks have to agree rather than race. - **Downgrade from N slots to M:** the owner picks which campaigns stay active; the rest go read-only. Nothing is chosen silently. **BYO-AI (v4.2.0) is a natural pricing lever.** A campaign using its own provider key consumes no host AI capacity. ## Open questions - Pricing: the slot price, the AI unit (hours vs credits), whether allowances pool across an owner's campaigns or sit per campaign, and whether unused allowance rolls over. - Whose AI allowance a campaign draws on: the owner's plan, presumably, since they own the slot. - Do BYO-AI campaigns draw from the allowance at all (host ASR with BYO LLM is a mixed case)? - A trial: can a new owner try a campaign before paying? "No free tier" rules out free forever, not necessarily a trial. - Can someone with no slot still *create* a draft campaign and invite players before paying? - Member caps per plan: needed at all, given tables are 3–7 people? - Payment provider. Stripe is the usual choice. A merchant of record (Paddle, Lemon Squeezy) handles international VAT and sales tax, which matters for a solo operator selling abroad. - Should one hosted Discord bot serve every guild? Today one bot serves one instance. - **Hosting prerequisites that aren't billing:** terms of service, privacy policy, and data-processing terms for voice recordings. The standing-consent model in `CLAUDE.md` assumes groups who know each other; with strangers able to join a hosted table, the join step is where standing consent would need to be stated. *To be decided, not a per-session consent gate.* ## Phasing 0. **#643**, campaign invites with expiry, usage limits and per-campaign approval, including the `INVITE_CODE` gate fix. **#644**, campaign ownership. Independent of each other; both useful to self-hosters now. 1. **Entitlements interface** with the self-hosted "unlimited" provider, and the enforcement points wired but inert. No payments. 2. **Hosted provider:** plan tables, payment-provider webhooks, lapse and downgrade handling. 3. **AI metering and allowance enforcement**, once the pricing model is chosen. ## Related - #534: the invite link vs `INVITE_CODE` investigation that started this. - #357: per-session cost telemetry, the metering foundation. - v4.2.0: BYO-AI and provider abstraction. - #415: bot event stream sized for hosted scale. *Revised 2026-09-13: the draft's `slot_owner_id` (a billing-only field) was replaced by campaign ownership (#644), after the owner pointed out that the payer may be a player rather than a GM.*
Author
Collaborator

Answers from the redesign mockups (#659, lane 1b, 2026-09-18). They came up while the owner reviewed the Plan & billing prototype (HOME-7). All of them are recorded in the surface catalogue as D49 and IS-362–IS-376.

Payments: no card data in Quest Board. This is the owner's decision.

  • Quest Board will not process or store cards itself. Paying happens on a hosted payment provider's own page (Stripe or similar), which keeps Quest Board out of PCI scope. In Stripe terms, that is hosted Checkout plus the Customer Portal. It also fits the webhook-synced plan tables above, where request handling never calls the provider.
  • The prototype shows:
    • buying credits as a trip to the provider and back, where the page waits for the provider to confirm rather than claiming success;
    • payment details and invoices linked out to the provider's page;
    • a failed payment as "the provider retries and emails", followed by the grace period and then read-only.
  • Adding a slot to a plan that already pays only asks for confirmation. A first slot goes to the provider's page.
  • Patreon for extra perks is the owner's idea. It is noted, not decided.
  • The provider itself is still open, including the merchant-of-record question above.

Pricing: provisional working figures from the owner, who may change them.

  • $4 a month per campaign slot. This replaces the $3 working figure, and no AI is included.
  • The AI unit is credits: $1 buys 60. An hour of transcription and summary costs 60 credits, and the other AI features cost fewer. The smaller costs in the prototype are placeholders.

Two open questions answered the simple way in the prototype. The owner has not objected to either.

  • Credits belong to the owner and are shared across the campaigns they own. Each campaign's use is shown separately.
  • A campaign on its own AI keys uses no credits. The mixed case of host ASR with a BYO LLM is still open.

Prototype: https://questboard-dev.rhoving.com/feedback/prototype/lane1b?only=billing

**Answers from the redesign mockups (#659, lane 1b, 2026-09-18).** They came up while the owner reviewed the Plan & billing prototype (HOME-7). All of them are recorded in the surface catalogue as D49 and `IS-362`–`IS-376`. **Payments: no card data in Quest Board. This is the owner's decision.** - Quest Board will not process or store cards itself. Paying happens on a hosted payment provider's own page (Stripe or similar), which keeps Quest Board out of PCI scope. In Stripe terms, that is hosted Checkout plus the Customer Portal. It also fits the webhook-synced plan tables above, where request handling never calls the provider. - The prototype shows: - buying credits as a trip to the provider and back, where the page waits for the provider to confirm rather than claiming success; - payment details and invoices linked out to the provider's page; - a failed payment as "the provider retries and emails", followed by the grace period and then read-only. - Adding a slot to a plan that already pays only asks for confirmation. A first slot goes to the provider's page. - **Patreon for extra perks** is the owner's idea. It is noted, not decided. - The provider itself is still open, including the merchant-of-record question above. **Pricing: provisional working figures from the owner, who may change them.** - **$4 a month per campaign slot.** This replaces the $3 working figure, and no AI is included. - **The AI unit is credits:** $1 buys 60. An hour of transcription and summary costs 60 credits, and the other AI features cost fewer. The smaller costs in the prototype are placeholders. **Two open questions answered the simple way in the prototype.** The owner has not objected to either. - **Credits belong to the owner and are shared** across the campaigns they own. Each campaign's use is shown separately. - **A campaign on its own AI keys uses no credits.** The mixed case of host ASR with a BYO LLM is still open. Prototype: https://questboard-dev.rhoving.com/feedback/prototype/lane1b?only=billing
Author
Collaborator

Decision (owner, 2026-10-12): the public app and a private hosted add-on

The owner's aim, in their words: "I don't want to make it easier than I need to for a selfhoster to sell the same services I'm selling… I'm not going to place restrictions on selling that service, but I don't want to make it too easy for somebody just fork the code and start competing with me directly." And: "If somebody really wants to set up a service and run it, more power to them. I just don't want to make it too easy."

Decided

  1. One public repository for the app, AGPL-3.0, and no private fork of it. The hosted service runs the same app as everyone else.
  2. The public app carries only neutral hooks. At each point where a limit could apply (creating, un-archiving or accepting a campaign; redeeming an invite; starting a recording; process_audio, the LLM fan-out and the on-demand generators) the app asks "may this happen?", and a self-hosted server always answers yes. Archive and read-only stay in the app; they are already self-hosters' features.
  3. A private add-on carries everything that makes it a business: plans, slots and the credit ledger and their tables; what each AI feature costs; lapse, grace and downgrade; the Plan & billing page and Admin → Plans & entitlements; the Stripe and Patreon integrations and their webhooks. The hosted image is the public image plus the add-on.
  4. Order: the server side moves first (tables, logic, payment integrations: most of the value); the two pages follow.
  5. No licence restriction on selling hosting is added. The protections are the add-on's privacy, AGPL's publish-your-changes rule for the app itself, and the name (a trademark is the owner's to look into).

This revises the record above: "the entitlements interface" of phase 1 stays as the seam, but the plan model no longer lives behind it in the public app. What exists in the public repository today and moves out: app/services/entitlement_service.py's plan, slot, credit and payment shapes, app/routers/plan.py, the pricing settings (settings_service.get_plan_pricing, D71) and their Admin section (D144), and the Plan & billing page (HOME-7, D49). They remain in the history; they are thin and inert.

What has to be built for it

  • Add-on discovery in the backend: an installed package registers its provider (today PROVIDERS is a fixed dict), its routers, its Celery tasks and its Alembic migrations (a separate version location or branch, so the public migration history never mentions billing).
  • A frontend extension point: the hosted build adds its pages and account-menu entries; the public build has none.
  • The neutral hooks themselves, with a contract stable enough that the add-on does not chase every release.
  • A private repository for the add-on, and a hosted image built from the public image plus it.

Caveats recorded with the decision

  • This raises the cost of competing; it does not prevent it.
  • The arrangement relies on the owner holding the copyright in the whole app. Before outside contributions are accepted, a contributor agreement is needed to keep the right to combine the app with a private add-on. Not legal advice; worth a check.
## Decision (owner, 2026-10-12): the public app and a private hosted add-on **The owner's aim, in their words:** "I don't want to make it easier than I need to for a selfhoster to sell the same services I'm selling… I'm not going to place restrictions on selling that service, but I don't want to make it too easy for somebody just fork the code and start competing with me directly." And: "If somebody really wants to set up a service and run it, more power to them. I just don't want to make it too easy." **Decided** 1. **One public repository for the app, AGPL-3.0, and no private fork of it.** The hosted service runs the same app as everyone else. 2. **The public app carries only neutral hooks.** At each point where a limit could apply (creating, un-archiving or accepting a campaign; redeeming an invite; starting a recording; `process_audio`, the LLM fan-out and the on-demand generators) the app asks "may this happen?", and a self-hosted server always answers yes. Archive and read-only stay in the app; they are already self-hosters' features. 3. **A private add-on carries everything that makes it a business:** plans, slots and the credit ledger and their tables; what each AI feature costs; lapse, grace and downgrade; the Plan & billing page and Admin → Plans & entitlements; the Stripe and Patreon integrations and their webhooks. The hosted image is the public image plus the add-on. 4. **Order:** the server side moves first (tables, logic, payment integrations: most of the value); the two pages follow. 5. No licence restriction on selling hosting is added. The protections are the add-on's privacy, AGPL's publish-your-changes rule for the app itself, and the name (a trademark is the owner's to look into). **This revises the record above:** "the entitlements interface" of phase 1 stays as the seam, but the plan model no longer lives behind it in the public app. What exists in the public repository today and moves out: `app/services/entitlement_service.py`'s plan, slot, credit and payment shapes, `app/routers/plan.py`, the pricing settings (`settings_service.get_plan_pricing`, D71) and their Admin section (D144), and the Plan & billing page (HOME-7, D49). They remain in the history; they are thin and inert. **What has to be built for it** - Add-on discovery in the backend: an installed package registers its provider (today `PROVIDERS` is a fixed dict), its routers, its Celery tasks and its Alembic migrations (a separate version location or branch, so the public migration history never mentions billing). - A frontend extension point: the hosted build adds its pages and account-menu entries; the public build has none. - The neutral hooks themselves, with a contract stable enough that the add-on does not chase every release. - A private repository for the add-on, and a hosted image built from the public image plus it. **Caveats recorded with the decision** - This raises the cost of competing; it does not prevent it. - The arrangement relies on the owner holding the copyright in the whole app. Before outside contributions are accepted, a contributor agreement is needed to keep the right to combine the app with a private add-on. Not legal advice; worth a check.
Author
Collaborator

Owner's answers, 2026-10-12 (Questions page):

  • Q177 (a): a trial. One free 30-day campaign per account, no card asked; then read-only until a slot is bought. No AI credits in the trial.
  • Q178 (a): one shared Quest Board bot that every customer adds to their server.
  • Q179 (a): mixed AI. Credits are charged only for the part that runs on the service's hardware, at that part's own rate.
  • Q180 (b): Patreon tiers include slots or credits, as a second way to pay (against my recommendation of perks only). So the add-on has two payment sources feeding one plan state, and lapse, refund and downgrade need defining for both.
  • Q175 (who takes payments): not answered; the owner is thinking about the merchant-of-record question.
  • Q176 (sign-up): not answered as asked. The owner, in chat: not their private Authentik, no passwords stored in Quest Board, and no sign-in with Discord or Google. Options are being discussed; the outcome goes on #1148.
**Owner's answers, 2026-10-12 (Questions page):** - **Q177 (a): a trial.** One free 30-day campaign per account, no card asked; then read-only until a slot is bought. No AI credits in the trial. - **Q178 (a): one shared Quest Board bot** that every customer adds to their server. - **Q179 (a): mixed AI.** Credits are charged only for the part that runs on the service's hardware, at that part's own rate. - **Q180 (b): Patreon tiers include slots or credits,** as a second way to pay (against my recommendation of perks only). So the add-on has two payment sources feeding one plan state, and lapse, refund and downgrade need defining for both. - **Q175 (who takes payments):** not answered; the owner is thinking about the merchant-of-record question. - **Q176 (sign-up):** not answered as asked. The owner, in chat: not their private Authentik, no passwords stored in Quest Board, and no sign-in with Discord or Google. Options are being discussed; the outcome goes on #1148.
Author
Collaborator

Owner answers from the Questions page (14 Oct 2026):

  • Q175 (who takes the payments): (c) build it so either a merchant of record or Stripe can be plugged in, and choose when the service is nearly ready.
  • Q176 (how strangers sign up): already decided in chat on 12 Oct and recorded on #1148: passwordless sign-in (an emailed link or code, and passkeys), built into the public app, with OIDC kept for instances that use it. No passwords, no Discord or Google sign-in, not the owner's private Authentik. (Corrected 17 Oct: an earlier version of this comment called the answer empty and re-asked it as Q182. That was my mistake, and Q182 is withdrawn.)

Q177–Q180 were recorded on 12 Oct. Every question in this set is now answered.

Owner answers from the Questions page (14 Oct 2026): - **Q175 (who takes the payments):** (c) build it so either a merchant of record or Stripe can be plugged in, and choose when the service is nearly ready. - **Q176 (how strangers sign up):** already decided in chat on 12 Oct and recorded on #1148: **passwordless sign-in (an emailed link or code, and passkeys), built into the public app**, with OIDC kept for instances that use it. No passwords, no Discord or Google sign-in, not the owner's private Authentik. (Corrected 17 Oct: an earlier version of this comment called the answer empty and re-asked it as Q182. That was my mistake, and Q182 is withdrawn.) Q177–Q180 were recorded on 12 Oct. Every question in this set is now answered.
Author
Collaborator

Owner's decisions, 17 Oct 2026 (chat):

  • Prices are decided at launch. Until then the working figures stay as they are: $4 a month per campaign slot, and $1 buys 60 AI credits.
  • Support access follows the same rule as admin access (Q181, #1181). By default the operator sees only the metadata of a customer's campaign: what is in it, sizes, and who added what. Opening the content itself (a page, a file, a transcript) is a deliberate act, written to the audit log, and stated in the Privacy page's "who can see what" and in the hosted service's terms (#1151). Nothing gives the operator quiet, unaudited access to campaign content.
**Owner's decisions, 17 Oct 2026 (chat):** - **Prices are decided at launch.** Until then the working figures stay as they are: $4 a month per campaign slot, and $1 buys 60 AI credits. - **Support access follows the same rule as admin access (Q181, #1181).** By default the operator sees only the metadata of a customer's campaign: what is in it, sizes, and who added what. Opening the content itself (a page, a file, a transcript) is a deliberate act, written to the audit log, and stated in the Privacy page's "who can see what" and in the hosted service's terms (#1151). Nothing gives the operator quiet, unaudited access to campaign content.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#642
No description provided.