LoguDocs
Open Logu

Plans and billing

Every workspace is on a plan: Free or Pro. Personal and team workspaces alike; a workspace's Owners pay for it. Design notes: docs/superpowers/specs/2026-10-13-billing-design.md.

Two layers keep cost bounded (below): cost ceilings that are always on, and plan quotas that apply when limits are enforced. A third set of limits is per account, not per workspace, and is always on too: how many workspaces one person may own, and the daily caps and Turnstile check at the door (Abuse protection).

Plans#

Free Pro
Price US$ 0 US$ 29/month per workspace (founder prices below)
Members 1 (the Owner) as many as you invite
Asks 100 a month 5,000 a month (fair use)
Events 1,000 a month 50,000 a month (fair use)
Metric reports 10,000 a month 250,000 a month (fair use)
Image storage 200 MB 20 GB
Test API key traffic 100 asks, 1,000 events, 10,000 metric reports a month the cost ceilings only
Emails Logu sends for the workspace 300 a month the cost ceiling only (5,000)
Image uploads 400 a month the cost ceiling only (20,000)
WhatsApp template messages none 100 a month
Projects 5 100
Metrics (names) 20 500
Agents 2 no cap
Webhook endpoints none up to 10
History in the app the last 30 days everything kept (180 days)
Deliveries (push, browser, Telegram, email) all all

Monthly counts are per calendar month in UTC and reset on the 1st. Retention is 180 days on every plan: a Free workspace that upgrades sees its older history again. Every number is a constant in the protocol (FREE_PLAN_LIMITS, PLAN_QUOTAS, COST_CEILINGS, PRICING in packages/protocol/src/billing.ts): change it there.

Founder prices. The first 100 workspaces that subscribe pay US$ 9/month, the next ones up to 1,000 in all pay US$ 19/month, then US$ 29. The price is locked for as long as that subscription stays active; cancel and subscribe again and you get the price of the day. A checkout holds its spot for about 45 minutes (a 30-minute Checkout Session plus a grace period); an abandoned one gives it back. A member holds at most one open reservation across all their workspaces, and may start at most 5 Checkout Sessions an hour (see Founder slots below). The landing page and Settings → Plan show the current price and the spots left (GET /v1/billing/pricing).

Cost ceilings and plan quotas#

Cost was unbounded: Pro had no volume cap, WhatsApp and email were uncapped, image storage was unmetered, test API keys weren't counted. Two layers fix that. Both are per workspace, checked on the server, counted in one statement with the increment (concurrent requests can't go over), and a thing that is refused is never created.

Cost ceilings Plan quotas
Applies always, whatever BILLING_ENFORCED says only when BILLING_ENFORCED is "true"
Meant to be generous: no honest workspace meets one what the plan includes
Over it 402 ceiling_reached 402 plan_limit
Numbers COST_CEILINGS PLAN_QUOTAS (Free, Pro), times QUOTA_FACTOR per price tier

A meter's limit is the lower of the two. Counting never stops while limits aren't enforced, so turning enforcement on starts from the true usage.

The constants#

Every number, and where it lives. All in packages/protocol/src/billing.ts unless it says otherwise. A ceiling or a quota with no value for a plan means that layer doesn't apply to it.

Meter Cost ceiling (COST_CEILINGS) Free quota Pro quota Counted in
Asks a month 20,000 asksPerMonth 100 5,000 workspace_usage.asks
Events a month 200,000 eventsPerMonth 1,000 50,000 workspace_usage.events
Metric reports a month 1,000,000 metricsPerMonth 10,000 250,000 workspace_usage.metrics
Test asks a month (test API keys) 5,000 testAsksPerMonth 100 none workspace_usage.test_asks
Test events a month 50,000 testEventsPerMonth 1,000 none workspace_usage.test_events
Test metric reports a month 250,000 testMetricsPerMonth 10,000 none workspace_usage.test_metrics
WhatsApp template messages a month 500 whatsappPerMonth 0 100 workspace_usage.whatsapp
Emails Logu sends for the workspace a month 5,000 emailsPerMonth 300 none workspace_usage.emails
Emails one member receives a day (across workspaces) 100 emailsPerMemberPerDay none none member_counter (kind emails)
Image uploads a month 20,000 imagesPerMonth 400 none workspace_usage.images
Image storage (bytes held now) 25 GB storageBytes 200 MB 20 GB workspace_storage.bytes
Projects 500 projects 5 100 counted from project
Metrics (names; a test metric counts too) 2,000 metricNames 20 500 counted from metric
Active API keys of one agent 20 apiKeysPerAgent none none counted from api_key
Requests a minute per workspace (all its API keys and connectors) 3,000 RATE_LIMITS.workspaceRequestsPerMinute n/a n/a WORKSPACE_RATE_LIMITER (apps/api/wrangler.jsonc, namespace 4109)
Checkout Sessions a member starts an hour 5 RATE_LIMITS.checkoutsPerHour n/a n/a member_counter (kind checkout)
Quota warning share 80% QUOTA_WARNING_RATIO
Quota factor per price tier QUOTA_FACTOR: founder_9 1, founder_19 1, pro_29 1

Storage uses binary units (1 GB = 1,024 MB). The per-key rate limits (RATE_LIMITS: 60 asks, 600 events and metrics, 120 images and 1,200 requests a minute) are unchanged; the workspace limit counts the requests of every key together, so more API keys don't lift it.

What a limit looks like#

Going over a limit answers 402 with what was hit and a hint (docs/errors.md). Nothing is created: an ask over a limit was not asked, and the agent is told so (the SDKs raise PlanLimitError, for both codes; MCP tools return a tool error starting plan_limit: or ceiling_reached:). Agents must not treat it as an answer, and must tell their human.

  • plan_limit: the plan's quota (Free and Pro). Hint: upgrade (Free) or ask the Logu team to raise the workspace's quota (Pro). Pro has no overage billing yet.
  • ceiling_reached: Logu's ceiling, which upgrading doesn't lift. Hint: tell your human, don't retry before the 1st, the Logu team can raise it for a workspace.

What each meter refuses:

  • Asks, events, metric reports (and their test meters), image uploads: the request is refused until the 1st (UTC). A refused request isn't counted, and a retry of an accepted one (same Idempotency-Key) isn't counted twice.
  • Image storage: the upload is refused and nothing is stored; sending the ask or event without the image works. Storage counts unused uploads too (deleted after 24 hours) and gives bytes back when images are deleted by that cleanup or by retention (180 days). The counter is kept incrementally and reconciled against the attachment rows by the daily maintenance.
  • Projects, metric names, API keys: the request that would create one more is refused; the ones that exist keep working. Logu's own notices go to a project named logu that isn't counted.
  • Requests a minute per workspace: 429 rate_limited with Retry-After, like the per-key limits.
  • A third agent on Free (POST /v1/agents, connecting a new agent with logu login or OAuth): refused; connecting as an existing agent still works. Inviting members, and creating webhook endpoints: refused on Free. Members and endpoints a workspace already has stay; its endpoints are paused (no webhook deliveries) while it's on Free.
  • History: members see the last 30 days of asks, events and the activity log on Free (asks still waiting always show). Agents read everything.

Delivery channels never block an ask#

WhatsApp template messages (Meta bills each one; a free-form message inside the 24-hour window is free and isn't counted) and emails are capped, not refused. When a channel's cap is reached the ask is created and lands in the app and on every other delivery; only that channel skips the send. Free has no WhatsApp templates (quota 0); Pro has 100 a month. A skipped send:

  • bumps the ops counter delivery_capped (delivery_capped_5m in the ops project);
  • writes one delivery.capped activity entry a month per channel (data: channel, reason: quota, ceiling or member_daily);
  • shows Owners a note on Settings → Plan ("WhatsApp template messages were skipped this month…").

Emails are counted for asks, reminders, "Send a test" and invitations (an invitation whose email is capped still returns its link to share by hand). Sign-in and confirmation emails belong to no workspace: they are limited per IP address and per email address, and a global daily cap bounds the ones sent to addresses with no account or no verified email (Abuse protection). A send the provider rejects gives its unit back.

WhatsApp templates are only for an ask and its reminder. Outside the 24-hour window an ask (and its reminder) goes out as a template, which is billed; an outcome ("Ana decided") and an event have no template: inside the window they are free-form messages, outside it they are not sent over WhatsApp (they stay in the app, on push and on the other deliveries, and a skipped one costs nothing and counts nowhere). That is two billed templates per ask per person where it was three. See delivery.md.

A WhatsApp credit pack (extra template messages sold apart from the plan) is not built: the flag WHATSAPP_CREDIT_PACK.enabled (false) and messagesPerPack in the protocol mark where it plugs in; today nothing reads a workspace's credits.

Telling the Owners#

Only for plan quotas, so only while limits are enforced: when a meter reaches 80% and again at 100% of its quota, the workspace's Owners get a notice, once a month per meter:

  • an activity entry (billing.quota_warning at 80%, billing.quota_reached at 100%);
  • an event from Logu in the project logu (like a threshold alert), delivered to the Owners following each one's delivery settings (push, Telegram, quiet hours, a muted project). Never an agent path: agents can't decide, change or read any of it.

A cost ceiling reached writes billing.ceiling_reached and the same event, once a month per meter, and bumps the ops counter ceiling_hit. Settings → Plan shows every meter: the bar turns to a warning (an icon and "Nearly full") from 80% and to "Full" at 100%.

Raising a limit for one workspace#

An operator sets limits by hand in D1, never through the API: workspace_subscription.limit_overrides is a JSON object with any of the names in the table above (asksPerMonth, storageBytes, …). Each key replaces both the ceiling and the plan quota of that name for that workspace (so a Pro workspace that needs 30,000 asks gets {"asksPerMonth": 30000}). Values are whole numbers of 0 or more; anything invalid (bad JSON, an unknown name, a negative number) is ignored, so a typo can't lift a ceiling. No API route reads or writes it, and the API never shows it: Owners see the effective numbers on Settings → Plan.

pnpm --filter @logu/api exec wrangler d1 execute logu-db --remote --command \
  "INSERT INTO workspace_subscription (workspace_id, limit_overrides)
   VALUES ('wsp_…', '{\"asksPerMonth\": 30000, \"storageBytes\": 53687091200}')
   ON CONFLICT (workspace_id) DO UPDATE SET limit_overrides = excluded.limit_overrides"

Undo with UPDATE workspace_subscription SET limit_overrides = NULL WHERE workspace_id = 'wsp_…'. Overrides apply at the next request; they work on Free and Pro, with or without BILLING_ENFORCED.

Founder slots: hoarding#

A founder price is a scarce thing, so a checkout can't be used to sit on spots:

  • A member holds at most one open reservation across all their workspaces (founder_slot.user_id). Starting a checkout for a second workspace while the first is open still works, at the list price, until the first is paid, expires (Stripe's checkout.session.expired) or lapses. The workspace that holds the slot keeps it when it starts again.
  • A member may start at most 5 Checkout Sessions an hour (429 rate_limited).
  • A reservation still lasts about 45 minutes, and the claim is one atomic statement, so concurrent checkouts of one member get one slot.

Abuse protection#

Phase 1 of the cost plan (docs/business/cost-plan/, impl-01-abuse-protection.md). Per-workspace ceilings only bound the bill if workspaces and accounts can't be made without limit, so there are three controls at the account and the door. All are always on, whatever BILLING_ENFORCED says.

Workspaces per account#

Limit Constant (packages/protocol/src/billing.ts)
Shared workspaces an account may own, no workspace on Pro or comped 3 WORKSPACE_LIMITS.ownedFree
The same, owning at least one workspace on Pro or comped (the personal one too) 25 WORKSPACE_LIMITS.ownedPaid
Workspaces created per account per UTC day 5 WORKSPACE_LIMITS.createdPerDay
  • Owned is a membership with the role Owner in a shared workspace; the personal workspace never counts. Being a Decider or Viewer of other people's workspaces never counts and is never blocked: an account at its limit still joins any number of workspaces it is invited to.
  • A verified email is needed to create a workspace and to become the Owner of one (403 email_not_verified). Becoming an Owner (an Owner promoting a member, or accepting an invitation as Owner) counts against that person's limit too; an Owner invitation that finds the person at the limit stays pending and says why when accepted by hand (it is not accepted automatically at sign-in).
  • Accounts above a limit keep everything (grandfathered): nothing is removed, renamed or blocked, they just can't create or take more until they are under it again (delete a workspace, or have one on Pro).
  • The count and the insert are one D1 batch (the workspace row goes in only while the account owns fewer than its limit), and the daily count is claimed atomically in member_counter (kind workspaces, one row per account per UTC day, deleted after two days). Two requests at once can't both pass, and one that loses the race gives its daily unit back.
  • Errors (docs/errors.md), all with a hint and nothing created: a Free account at its 3 gets 402 plan_limit ("Upgrade one of your workspaces to Pro to own up to 25, or write to the Logu team if you are an agency or consultant"); an account at 25, or at a number set by hand, gets 402 ceiling_reached; the 6th creation of the day gets 429 rate_limited with Retry-After until 00:00 UTC; an unverified email gets 403 email_not_verified. The app's create-workspace dialog says each in the user's language (workspace_limit_*, workspace_create_*).
  • Activity: a creation is workspace.created; a refusal is workspace.create_refused in the account's personal workspace (data.reason: owned_limit, daily_limit, email_not_verified), at most once a day per reason, so a script can't fill the log.
  • API keys and OAuth tokens (agents) can't create workspaces at all (POST /v1/workspaces is for signed-in humans), and nothing in the API reads or writes the overrides below.

For agencies and consultants. Raise the numbers for one account by hand, in D1, never through the API (invisible to agents and OAuth tokens). account_limit.overrides is a JSON object with ownedWorkspaces (replaces 3 or 25) and/or workspacesPerDay (replaces 5), whole numbers of 0 or more; anything invalid is ignored, so a typo can't lift a limit.

pnpm --filter @logu/api exec wrangler d1 execute logu-db --remote --command \
  "INSERT INTO account_limit (user_id, overrides, note)
   SELECT id, '{\"ownedWorkspaces\": 40, \"workspacesPerDay\": 10}', 'Acme agency, 2026-10-17'
   FROM user WHERE email = 'ana@agency.example'
   ON CONFLICT (user_id) DO UPDATE SET overrides = excluded.overrides, note = excluded.note"

Undo with DELETE FROM account_limit WHERE user_id = (SELECT id FROM user WHERE email = '…'). It applies at the next request.

Rolling back. There is no flag: set WORKSPACE_LIMITS in the protocol high and deploy, or put an override with large numbers on the accounts that need it.

Cloudflare Turnstile#

Cloudflare's free captcha on password sign-up, password sign-in and magic-link requests. Not on passkey sign-in, the magic link's click, the confirmation link, the device-code connect or the OAuth connect (those sit behind an existing session or prove an email), and never on anything an agent calls.

  • Off until you set it up. The API enforces it only while the secret TURNSTILE_SECRET_KEY is set, and the app shows it only when it was built with VITE_TURNSTILE_SITE_KEY. Unset (local development, tests, e2e, self-hosted servers) nothing changes. The code ships first; you turn it on.
  • What the API does. The app sends the widget's token in the Turnstile-Token header; the API checks it with Cloudflare's siteverify (a fetch with a 5-second timeout) after the per-IP and per-address rate limits, so a flood never reaches Cloudflare. No token or a rejected one: 403 forbidden ("Complete the verification and try again."). Cloudflare unreachable, slow or answering nonsense: fail closed, 503 service_unavailable ("Try again in a minute."). Only Cloudflare's result code is logged (turnstile.rejected, turnstile.siteverify), never the token, the secret or the address.
  • What the app does. The script from challenges.cloudflare.com is loaded when a sign-in or sign-up screen opens (never in the initial bundle: the 180 KB budget is untouched), the widget runs on demand at each attempt with appearance: interaction-only (invisible unless Cloudflare wants an interaction, which then shows under the card), and every attempt (the resend of a link too) gets a fresh single-use token. It follows the app's light or dark theme and language (en, pt-br), the iframe is Cloudflare's accessible one, and a failure is said in the app's words (auth_error_turnstile).
  • CSP (apps/web/public/_headers): script-src, connect-src and a new frame-src allow exactly https://challenges.cloudflare.com and nothing else; test/csp.test.ts keeps it honest (no wildcard, no unsafe-inline, and the script's URL appears in the build only as that one URL).
  • The iOS and Android apps (Capacitor) load the same bundle, so the widget runs there too: add localhost to the widget's hostnames (the web view's origin is capacitor://localhost on iOS and https://localhost on Android). The over-the-air bundle carries the site key it was built with. An installed app from before this release has no widget: once the secret is set, its password sign-in and magic-link requests fail with the "couldn't confirm you're a person" message until it updates (sessions already signed in keep working). Raise MIN_NATIVE_VERSION only if you want to force it.
  • Cloudflare's test keys (always passes 1x00000000000000000000AA / secret 1x0000000000000000000000000000000AA, always blocks 2x…AB / 2x…AA, dummy token XXXX.DUMMY.TOKEN.XXXX) appear only in tests, with siteverify mocked.

What you do, once (free):

  1. Cloudflare dashboard → Turnstile → Add widget. Name "Logu sign-in"; hostnames logu.dev and localhost (for the apps; add localhost also for local development if you want to try it); mode Managed; pre-clearance off. Copy the site key (public) and the secret key.
  2. The site key is public and committed in apps/web/.env.production (VITE_TURNSTILE_SITE_KEY), so every production build (Workers Builds, cap:sync, the over-the-air bundle) shows the widget; the e2e build overrides it with an empty value. Deploy logu-web and check the widget on logu.dev before the API asks for it (until the secret is set the API ignores the token).
  3. Then set the secret on the API: pnpm --filter @logu/api exec wrangler secret put TURNSTILE_SECRET_KEY. From the next request, the three routes need a token.
  4. Try a sign-in, a sign-up and a magic link on logu.dev. To turn it off again, delete the secret (wrangler secret delete TURNSTILE_SECRET_KEY): no deploy needed, the app's widget keeps working and is simply not checked.

Global daily caps#

Whatever Turnstile and the rate limits let through, two counters bound a day (UTC), one D1 row each in global_counter (deleted after two days):

Cap Default Var in apps/api/wrangler.jsonc Counts
New accounts a day 1,000 (GLOBAL_DAILY_CAPS.signups) SIGNUPS_PER_DAY_MAX a password sign-up of a new address, and a magic link that creates the account
Sign-in emails a day to addresses with no account or no verified email 5,000 (GLOBAL_DAILY_CAPS.authEmails) AUTH_EMAILS_PER_DAY_MAX magic links, and the confirmation link of a sign-up, to such addresses
  • An existing account is never capped. Password sign-in, a magic link to an address that exists with a verified email, passkeys and sessions don't touch either counter, so an attacker who uses the whole cap locks no one out; they only stop strangers from joining until 00:00 UTC. (An existing account whose email was never confirmed does count against the email cap.)
  • A refusal is a calm 429 rate_limited ("New sign-ups are temporarily limited. Try again in a few hours." / "Sign-in emails to new addresses are temporarily limited…") with Retry-After to 00:00 UTC, and the hint says that existing accounts are unaffected. A magic link to an unknown address is refused before any email is sent while sign-ups are closed. Both counters are one atomic statement, and a sign-up that fails or an email the provider rejects gives its unit back.
  • The vars are numbers; empty or invalid means the default. They live in wrangler.jsonc, so changing one is a deploy of the config (or the dashboard's variable, until the next deploy).
  • Monitoring (ops, observability.md): signups_capped_5m and login_emails_capped_5m count refusals and alert above 0; signups_1d and auth_emails_1d report today's totals and alert at 500 and 2,500 (half of the defaults). The alerts are added to an existing ops workspace on the first cron run after the deploy.

Before turning BILLING_ENFORCED on#

Quotas only bite when BILLING_ENFORCED is "true". Before you flip it, list every Free workspace that would be over a quota on day one (read-only, nothing is written):

pnpm --filter @logu/api billing:preflight

It runs apps/api/scripts/billing-preflight.sql against production D1 and prints four lists: Free workspaces over a monthly quota in the last three months (asks, events, metric reports, test traffic, WhatsApp, emails, image uploads), Free workspaces holding more than Free includes now (image bytes, projects, metric names, agents, members), Free workspaces with webhook endpoints (they would be paused), and accounts that own more than 3 shared workspaces with no Pro one. Contact or comp the real ones, set limit_overrides where an exception makes sense, then set BILLING_ENFORCED to "true" and deploy. Workspaces on Pro or comped are left out, and one with limit_overrides is listed by the default quotas (look at its row). test/billing-preflight.test.ts keeps the SQL's numbers equal to the protocol's.

Enforcement switch#

BILLING_ENFORCED (a var in apps/api/wrangler.jsonc) is "false": plan quotas aren't applied, usage is still counted, and the app shows plans, usage and Upgrade with a note that limits aren't applied yet. The cost ceilings and the per-account limits are on either way. Run the pre-flight above, then set it to "true" and deploy to enforce the quotas.

Upgrading and managing (Owners)#

Settings → Plan → Upgrade opens Stripe Checkout at the current price; Manage opens Stripe's Customer Portal (card, invoices, cancel). Cancelling keeps Pro until the end of the paid period. If a renewal payment fails, the workspace stays on Pro while Stripe retries (past_due); when Stripe gives up the subscription is cancelled and the workspace is Free. A workspace with an active subscription can't be deleted until it's cancelled. The iOS and Android apps show the plan but don't sell it: manage it on logu.dev.

Route Who
GET /v1/billing Owners plan, subscription, this month's usage, Free limits, every meter (meters: used, quota, ceiling, state), channels capped this month (deliveryCapped), enforced
POST /v1/billing/checkout Owners { url } of a Checkout Session
POST /v1/billing/portal Owners { url } of a Customer Portal session
GET /v1/billing/pricing anyone current price and founder spots left
POST /stripe/webhook Stripe (signed) subscription lifecycle

Activity log: billing.checkout_started, billing.subscribed, billing.updated, billing.canceled, billing.payment_failed, billing.quota_warning, billing.quota_reached, billing.ceiling_reached, delivery.capped, workspace.create_refused.

Setting up Stripe#

Nothing in Stripe is created by deploying. Run, with the Stripe key of the mode you want (test first):

CLOUDFLARE_ACCOUNT_ID=… pnpm stripe:setup https://api.logu.dev --dry-run   # look first
CLOUDFLARE_ACCOUNT_ID=… pnpm stripe:setup https://api.logu.dev

It creates (or finds) everything below, stores the two secrets on logu-api and prints the Price ids. By hand, it's:

  1. Product "Logu Pro" (metadata logu=pro).
  2. Three Prices on it, USD, recurring monthly: US$ 9 (lookup key logu_pro_founder_9), US$ 19 (logu_pro_founder_19), US$ 29 (logu_pro_29).
  3. Webhook endpoint https://api.logu.dev/stripe/webhook with the events checkout.session.completed, checkout.session.expired, customer.subscription.created, customer.subscription.updated, customer.subscription.deleted, invoice.payment_failed.
  4. Customer Portal (Settings → Billing → Customer portal): payment method updates, invoice history and cancellation (at the end of the billing period) on; switching plans off (it would let a customer move to a founder price); default redirect https://logu.dev/settings#plan.
  5. Secrets on the Worker: STRIPE_SECRET_KEY (a restricted key with write access to Checkout Sessions, Customer Portal sessions, Customers and Subscriptions is enough) and STRIPE_WEBHOOK_SECRET (the endpoint's whsec_…): pnpm --filter @logu/api exec wrangler secret put STRIPE_SECRET_KEY.
  6. Vars in apps/api/wrangler.jsonc: STRIPE_PRICE_FOUNDER_9, STRIPE_PRICE_FOUNDER_19, STRIPE_PRICE_PRO_29 (the price_… ids). With any of them or the key missing, Upgrade is disabled and POST /v1/billing/checkout answers 503; /stripe/webhook answers 404 without its secret.

Comping a workspace#

Pro without Stripe (the owner's own workspaces, partners) is a flag in D1, never an API route:

pnpm --filter @logu/api exec wrangler d1 execute logu-db --remote --command \
  "INSERT INTO workspace_subscription (workspace_id, comped) VALUES ('wsp_…', 1)
   ON CONFLICT (workspace_id) DO UPDATE SET comped = 1"

Undo with UPDATE workspace_subscription SET comped = 0 WHERE workspace_id = 'wsp_…'. A comped workspace shows "Pro · Comped" and has nothing to buy; its quotas are the list price's. Find a workspace's id in the app's URL (?workspace=) or with SELECT id, name FROM workspace WHERE name = '…'.

Data#

workspace_subscription (one row per workspace: Stripe customer and subscription, status, tier, period end, comped, limit_overrides), founder_slot (slot 1–1,000: reserved by an open checkout until it expires, taken once paid; user_id of the member who started it), workspace_usage (per workspace and month: asks, events, metric reports, the three test meters, WhatsApp template messages, emails, image uploads), workspace_storage (bytes of images held), workspace_notice (what was told once this month: quota notices, ceilings, capped channels), member_counter (emails a member received a day, checkouts a member started an hour, workspaces an account created a day and the once-a-day refusals' markers; deleted after two days), global_counter (new accounts and sign-in emails to new addresses, one row per name per UTC day; deleted after two days), account_limit (an account's hand-set limits), stripe_event (handled webhook event ids, 30 days, so a redelivery never runs twice).