Hosted service¶
How to run Weirling as a managed, multi-user service: plans, billing, abuse limits and account lifecycle.
Everything here applies only with DEPLOYMENT_MODE=hosted. A self-hosted
instance ignores it. Install and operations are the same as for self-hosting:
see Install & run.
A reader's journey¶
flowchart LR
accTitle: Hosted account lifecycle
accDescr: A reader signs up, confirms their email and gets a 30-day trial. Then they subscribe to Reader or Pro, or the account is paused and deleted 30 days later unless they subscribe.
signup["Sign up"] --> verify["Confirm email"]
verify --> trial["30-day trial"]
trial --> paid["Reader or Pro"]
trial --> paused["Paused"]
paused --> paid
paused --> deleted(["Deleted after 30 days"])
| Step | What happens | Setting |
|---|---|---|
| Sign up | Signups are capped per email domain per hour (20 times higher for large mail providers) | SIGNUP_PER_DOMAIN_PER_HOUR |
| Confirm email | No email except the confirmation until the link is clicked. Digests are built but not emailed; newsletters are dropped. A magic-link sign-in also confirms. | |
| Trial | No card needed. Emails when it starts, 5 days before it ends, and when it ends. | BILLING_TRIAL_DAYS, TRIAL_ENDING_NOTICE_DAYS |
| Trial over, no plan | The account is paused: read-only, no feed updates, no digests, no AI summaries. Reading, export, subscribing and deleting still work. A reminder goes out 7 days before deletion; the account and its data are deleted 30 days after the pause began. Subscribing at any point ends the pause. A lapsed subscription pauses the same way. | HOSTED_PAUSED_DELETE_DAYS (0 = never), HOSTED_DELETE_REMINDER_DAYS |
| Subscribe | Checkout through the payment provider; the plan applies when its webhook arrives | See Billing |
Plans and quotas¶
| Plan | Price | Feeds | AI summaries a day | Newsletters a day | Manual digest builds a day |
|---|---|---|---|---|---|
| Trial (30 days, no card) | Free | 2,000 | 1,000 | 300 (150 MB) | 12 |
| Reader | $6/month or $54/year | 500 | 200 | 100 (50 MB) | 6 |
| Pro | $12/month or $108/year | 2,000 | 1,000 | 300 (150 MB) | 12 |
There is no free hosted plan (D-18): the trial has everything in Pro, and an account with no plan after it is paused (see above).
Pro-only features¶
Pro (and the trial) also gets these; self-host has all of them (D-19, #246):
| Feature | Without Pro |
|---|---|
| Twice-daily digests | Choosing it is refused; an existing setting runs as daily after a downgrade |
| Story synthesis | Same-story cards still list the other sources, without the AI-written briefing; the creator summary is refused |
| API and MCP access | API tokens work only for the browser extension, which every plan can use |
| Priority fetching | Feeds keep their adaptive schedule; a feed with a Pro reader goes first in each polling run and is checked at least every HOSTED_PRO_MAX_POLL_MINUTES (60) after a clean poll |
- Prices are set in the payment provider (D-18); the app only knows the checkout links.
- Defaults are
DEFAULT_PLAN_QUOTASinbackend/app/config.py; 0 means unlimited. - Change a plan with
HOSTED_PLAN_QUOTAS, JSON merged over the defaults:HOSTED_PLAN_QUOTAS={"pro": {"sources": 3000}}. -
Change one account:
-
Daily allowances reset at midnight UTC. When AI summaries run out, the digest keeps the extract. Scheduled digests always run and never count as builds.
- Readers see it all in Preferences → Plan: the plans side by side, their limits and today's use.
Readers' own LLM keys¶
With SECRETS_ENCRYPTION_KEY set, a reader can add their own key in
Preferences → Your own AI key. Their summaries then run on their provider
account, outside the plan's AI allowance.
- Only providers in
BYOK_ALLOWED_BASE_URLSare accepted (Groq, OpenAI, OpenRouter, Together, Fireworks and DeepInfra by default), so a key can't make the server call any other host. - The key is tested with one tiny request, then stored encrypted with a key
derived from
SECRETS_ENCRYPTION_KEY(neverSECRET_KEY). - It is never returned by the API (only its last 4 characters) and is redacted from logs.
- It is used only for that reader's summaries, with no fallback to the instance's provider.
- Changing
SECRETS_ENCRYPTION_KEYmakes stored keys unreadable; readers then add theirs again.
A discounted plan for these readers is a pricing decision (#138). Add it as another plan when you decide.
Billing¶
Weirling supports two billing models on hosted instances:
- Merchant of record (Lemon Squeezy or Paddle): The provider acts as the merchant of record and handles VAT, GST, and sales tax filing across countries.
- Payment gateway (Razorpay): Razorpay operates as a payment gateway, not a merchant of record (Decision D-25). The instance operator is directly responsible for filing GST and any applicable sales tax.
Configuring Lemon Squeezy or Paddle¶
- Set
BILLING_PROVIDER(lemonsqueezyorpaddle) andBILLING_WEBHOOK_SECRET. - Point the provider's webhook at
https://<api>/api/v1/billing/webhook/<provider>, subscribed to subscription events. -
Create a product for each plan (Reader, Pro) with a monthly and a yearly price, and set their checkout links (Lemon Squeezy) or price ids (Paddle):
Setting Plan Billing period BILLING_CHECKOUT_READER_MONTHLYReader Monthly BILLING_CHECKOUT_READER_YEARLYReader Yearly BILLING_CHECKOUT_PRO_MONTHLYPro Monthly BILLING_CHECKOUT_PRO_YEARLYPro Yearly A plan with no link isn't offered. The older
BILLING_CHECKOUT_MONTHLYand_YEARLYstill work as Reader's links. -
List the ids that grant each plan in
BILLING_READER_PRICE_IDSandBILLING_PRO_PRICE_IDS(andBILLING_TEAM_PRICE_IDSfor hand-granted Team accounts). An id that isn't listed never upgrades anyone. - Set
BILLING_API_KEY. Paddle uses it for checkout and portal sessions; both providers use it to cancel a subscription when an account is deleted. Test Paddle withBILLING_SANDBOX=true.
Configuring Razorpay¶
- Set
BILLING_PROVIDER=razorpay. - Set API credentials and webhook secret:
- Create one Razorpay plan per plan and interval, then put the plan ids
where the other providers take checkout links, and list them for the
plan mapping:
(
BILLING_CHECKOUT_READER_MONTHLY=plan_... # Razorpay plan id BILLING_CHECKOUT_READER_YEARLY=plan_... BILLING_CHECKOUT_PRO_MONTHLY=plan_... BILLING_CHECKOUT_PRO_YEARLY=plan_... BILLING_READER_PRICE_IDS=plan_reader_monthly_id,plan_reader_yearly_id BILLING_PRO_PRICE_IDS=plan_pro_monthly_id,plan_pro_yearly_idRAZORPAY_PLAN_ID_PROstill works as a shortcut for a single Pro plan.) Subscriptions run 120 monthly or 10 yearly cycles. - Point your Razorpay webhook to:
https://<api>/api/v1/billing/webhook/razorpaySubscribe tosubscription.activated,subscription.charged,subscription.pending,subscription.halted,subscription.cancelled,subscription.completed, andsubscription.expired. - Tax responsibility: As Razorpay is a payment gateway, the operator must account for GST and foreign taxes on all subscription revenue.
- Customer portal: Razorpay has no hosted customer portal. Preferences >
Plan shows "Email to change or cancel", addressed to
GRIEVANCE_CONTACT_EMAIL(set it). The plan starts onsubscription.activatedorsubscription.charged, not onsubscription.authenticated(nothing has been charged yet). Account deletion cancels the active subscription immediately via Razorpay's Subscriptions API.
How billing behaves¶
- Webhooks are verified, recorded once (a replay does nothing) and applied in order, so a late, older event can't bring back a cancelled plan.
- A plan lapses at its expiry date, with no scheduled job.
- Switching plans happens in the provider's portal (Manage billing), which prorates, or via support for Razorpay. Checkout refuses a second subscription.
Abuse limits¶
| Limit | Default | Setting |
|---|---|---|
| Fetches per account per hour | 300 | HOSTED_FETCH_BUDGET_PER_HOUR |
| Polls of one host per hour, all accounts | 120 | HOSTED_HOST_POLLS_PER_HOUR |
| One account's feeds on the same host | 10 | HOSTED_MAX_SOURCES_PER_HOST |
| Inbound newsletters per account per day | 100 (25 MB) | HOSTED_INBOUND_PER_DAY, HOSTED_INBOUND_MB_PER_DAY |
A feed poll counts against every account that follows it, and is skipped only when all of them are over budget. These caps apply on top of the plan quotas.
Deleting an account¶
- The subscription is cancelled first.
- The data is erased even if the cancellation fails.
- The reader gets a confirmation email saying whether billing was cancelled.
When a cancellation fails
The attempt is recorded in billing_events (provider and subscription
id only) and the billing_cancel_failed alert fires for 7 days. Cancel
the subscription by hand in the provider's dashboard.