Skip to content

Gate Scheduling by plan, extend billing holds, and make HIPAA contract-only - #330

Merged
pumfleet merged 17 commits into
mainfrom
commercial-audit-fixes
Nov 4, 2026
Merged

pumfleet merged 17 commits into
mainfrom
commercial-audit-fixes

Conversation

@pumfleet

@pumfleet pumfleet commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Follows a commercial audit of what coss.com charges for. Scheduling can't charge more than $0.10 a booking, so serious use now pays through plans.

Scheduling plan gates (live environments only; sandbox has everything on every plan):

Free Pro Scale Enterprise
Live hosts 3 25 100 included, then $0.99 a month each 100 included, then contract
Holds No Yes Yes Yes
Embedded host portal, attribute routing No No Yes Yes
Seats per group session 10 50 10,000 10,000
Hosts per availability search 5 25 500 500
Live slot lists and searches a second 5 25 100 200
"Scheduling by coss.com" on live pages Shown Shown Removed Removed
  • scheduling.hosts is a new meter charged per day, like inboxes, first priced on the January 2027 card. Migration 0136 dates existing live hosts to 2027-01-01, and the worker's scheduling-host-fees loop charges them from then and dates any live host created without one during a rollout.
  • Existing setups keep working, but organizations already over a limit can't add more. Using a feature reads the plan in effect, and setup that outlives the request is also checked against a scheduled downgrade. A retry of an idempotency key still in its window meets no gate.

Billing holds and project budgets also stop new live bookings, Scheduling hosts, signature requests and inboxes, and close hosted forms. Holds already made still confirm, bookers and submitters never hear about the customer's billing, mail still arrives, and monthly charges continue.

Canadian inbound SMS is $0.0104 a segment from January 2027: numbers were sold there, but inbound invoiced as an uncharged line.

HIPAA is contract-only: turning it on needs a sales or internal contract, and billing still never turns it off. It's now on the security page, the plan table, the pricing FAQ and the Enterprise card. The Nylas comparison no longer says there's no BAA, and lists customers' own Google and Microsoft apps and custom booking domains as coming soon to Enterprise. Those two aren't built yet.

Video is gone: the meter, rate, product and SDK type, plus its row in the dashboard and iOS price lists. Sign's blank row is gone from those lists too. video stays a reserved org slug.

Failed-SMS fees were checked and left alone: Twilio's $0.001 applies only to messages ending Failed, and plan send limits cap the worst case at about $10 a day on Pro and $100 on Scale.

Before January 1

The Terms need 30 days' notice, so customers must be told by December 2 about the host fee, Canadian inbound SMS and the January SMS price rises. Nothing in this PR sends that notice.

Testing

  • bun run verify's steps on a fresh test database: biome ci (tracked directories only, since .claude/worktrees holds nested Biome configs), db:migrate:test, and turbo run typecheck test --affected, 73 of 73 tasks.
  • New apps/api/test/scheduling-plans.test.ts covers the gates, holds, replays and host fees end to end. The concurrent host-create test also passed with DATABASE_POOL_MAX=2.
  • Not run locally: the Playwright smoke tests (the merge queue runs them) and anything against real Stripe or Twilio.

Review

Twelve Codex rounds. Nothing was rejected.

  1. A new host could pass a capped plan's allowance; /holds skipped the routing gate; retries of bookings, series and signature requests were refused during holds; the footer read a scheduled downgrade's plan and showed in sandboxes.
  2. P1: creating a host held an advisory lock while the plan gate waited for a second connection, which could stall the pool; updating an event type had the same shape. Also plan gates on replays, host_filter: {} slipping past routing, and a test that would fail from January 2027.
  3. P1: the routing flag entered idempotency hashes, which would break retries across this deploy. Also expired keys skipping gates, and new paid hosts ignoring holds.
  4. Live hosts created by older tasks during a rollout would never count or pay.
  5. /slots/explain skipped the routing gate; searches narrowed as soon as a downgrade was scheduled, which I widened to every use-time gate.
  6. An unchanged host filter could be refused because jsonb reorders keys.
  7. Clean.
  8. Clean, after merging main again (its per-person booking limit touched the same code).
  9. Clean, after another merge of main (booking history and event type descriptions).
  10. Clean, after merging main again (host locale in createHost).
  11. Clean, after merging main again (threshold charging and credit limits).
  12. Clean, after merging main again (the Scale gate on calendar delegation, kept alongside these).

Noticed, not changed

  • createInbox has the same shape as the round 2 P1: it calls the plan gate inside a transaction holding an advisory lock.
  • packages/email/test/domains.test.ts:107 is flaky under load: it reads one of an organization's two tenant rows with no stream filter.
  • packages/push/test/push.test.ts:486 flaked once on the order of two devices.

…t-only

Scheduling can't charge more than $0.10 a booking, so serious use pays through
plans. Live environments only; sandbox has everything on every plan:
- hosts: 3 on Free, 25 on Pro, 100 included on Scale then $0.99 a month each
  (a new `scheduling.hosts` meter, charged by the day like inboxes, first priced
  on the January 2027 card; existing live hosts wait until then)
- holds on Pro and up; the embedded host portal, attribute routing and pages
  without "Scheduling by coss.com" on Scale and up
- seats per group session, hosts per availability search, and a plan rate for
  live slot lists and searches
What already exists keeps working; only new setup meets the plan.

Billing holds and project budgets now also stop new live bookings, signature
requests and inboxes, and close hosted forms. Holds already made still confirm.
People booking or submitting never hear about the customer's billing.

Price SMS received on Canadian numbers from January 2027: numbers are sold there
but inbound was invoiced as an uncharged line.

HIPAA mode needs a sales or internal contract, and is on the public site: the
security page, the plan table, the pricing FAQ and the Nylas comparison, which
also lists customers' own OAuth apps and booking domains as coming soon.

Remove the video meter, rate and product, which never shipped. The dashboard's
price list also drops meters without a price (Sign's blank row).
# Conflicts:
#	apps/site/src/app/(home)/pricing/page.tsx
The merged pricing tray fits at most four: SMS reminders stay in the text.
- A new host past a capped plan's allowance is refused, like a new inbox, so
  deleting and creating can't run past it.
- The holds route checks attribute routing, like bookings and series.
- A retry of a booking, series or signature request already made still answers
  with it during a billing hold, and sending a request already sent returns it.
- The footer reads the plan in effect, not what a scheduled downgrade allows, and
  never shows in a sandbox.
- Plan checks never run inside a transaction: creating a host held an advisory
  lock while the gate waited for a second connection, which could stall the pool
  under concurrent creates. Updating an event type had the same shape.
- A retry of a key already used meets no plan gate, for holds and API attributes.
- An empty host_filter needs routing too: it matches every active host.
- The host fee test no longer depends on today being before January 2027.
- gateRouting stays out of idempotency hashes, so retrying a request made before
  this deploys still replays it.
- An expired idempotency key makes something new, so it meets the plan's gates.
- A billing hold or project budget also stops new live hosts, each a new monthly
  charge.
Give a fee date to live hosts an older task created during a rollout, after the
migration ran: without one they would never count against the limit or pay.
# Conflicts:
#	AGENTS.md
#	apps/site/content/docs/(platform)/billing.mdx
#	apps/site/content/docs/scheduling/pools-and-groups.mdx
#	apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts
#	apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/hosts/page.tsx
#	packages/billing/src/catalog/plans.ts
#	packages/billing/test/engine.test.ts
#	packages/db/migrations/meta/0128_snapshot.json
#	packages/db/migrations/meta/_journal.json
#	packages/scheduling/src/event-types.ts
#	packages/scheduling/src/hosts.ts
- Slot explanations check attribute routing and the plan's slot rate, like slots.
- A scheduled downgrade changes what's in use only when it applies: searches,
  attributes, holds and the host portal read the plan in effect. Only setup that
  outlives the request, such as a host filter, is checked against the plan to come.
An unchanged host filter is compared canonically: jsonb doesn't keep key order, so
resending a filter that already exists could be refused as a change.
# Conflicts:
#	apps/site/src/lib/pricing.ts
#	packages/billing/AGENTS.md
#	packages/billing/src/catalog/entitlements.ts
#	packages/billing/src/catalog/plans.ts
# Conflicts:
#	packages/db/migrations/meta/0130_snapshot.json
#	packages/db/migrations/meta/_journal.json
@pumfleet
pumfleet added this pull request to the merge queue Nov 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 4, 2026
# Conflicts:
#	packages/db/migrations/meta/0131_snapshot.json
#	packages/db/migrations/meta/_journal.json
# Conflicts:
#	packages/db/migrations/meta/0134_snapshot.json
#	packages/db/migrations/meta/_journal.json
#	packages/scheduling/src/hosts.ts
# Conflicts:
#	packages/db/migrations/meta/0135_snapshot.json
#	packages/db/migrations/meta/_journal.json
@pumfleet
pumfleet enabled auto-merge November 4, 2026 22:32
@pumfleet
pumfleet disabled auto-merge November 4, 2026 22:33
# Conflicts:
#	apps/web/src/app/[orgSlug]/[projectSlug]/[envSlug]/scheduling/actions.ts
#	apps/web/src/lib/billing-preflight.ts
#	packages/billing/src/catalog/entitlements.ts
#	packages/billing/src/catalog/plans.ts
The calendar delegation test's fake gate needs hasFeature and currentLimit, and the
dashboard's Scheduling actions now reach the usage gate's system organization.
@pumfleet
pumfleet added this pull request to the merge queue Nov 4, 2026
Merged via the queue into main with commit c6451ae Nov 4, 2026
31 checks passed
@pumfleet
pumfleet deleted the commercial-audit-fixes branch November 4, 2026 22:44
@linear-code

linear-code Bot commented Nov 4, 2026

Copy link
Copy Markdown

ENG-8077

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant