Skip to content

fix(api): serialize the Telegram connect quota check under the company advisory lock - #384

Open
detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-api-serialize-the-telegram-connect-quota-check-f47f5a
Open

detail-app[bot] wants to merge 1 commit into
mainfrom
detail/bug-fix/fix-api-serialize-the-telegram-connect-quota-check-f47f5a

Conversation

@detail-app

@detail-app detail-app Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Detail bug report: View on Detail

Closes #379

Problem

The Telegram bot creation route (POST /channel-accounts/telegram-bot) enforced the paid connection-slot quota outside its insert transaction and took no advisory lock, while the WhatsApp spawnConnection path correctly serialized the same count-then-insert with pg_advisory_xact_lock(hashtextextended(companyId, 0)) inside its transaction. The check-then-insert on the Telegram side was a classic TOCTOU: under PostgreSQL's default READ COMMITTED, two concurrent Telegram connects (with distinct bot tokens) — or a Telegram connect racing a WhatsApp spawn — could both read the same pre-insert slot count, both pass used < max, and both insert, leaving the workspace above its plan limit. Over-limit channel_accounts rows persisted and kept counting toward countUsedConnectionSlots until manually archived. Introduced when the unified countUsedConnectionSlots quota landed (afe09e5); it added the shared count as a bare check on the Telegram path without the lock the WhatsApp path already held.

Fix

In apps/api/src/routes/channel-accounts.ts, move the getMaxConnections + countUsedConnectionSlots check inside the insert transaction and acquire the same company-scoped advisory lock spawnConnection uses (pg_advisory_xact_lock(hashtextextended(${companyId}, 0))), so the count+insert is atomic and serialized across both channels and all API replicas. The route's existing 402 response contract ({ error, code: "MAX_CONNECTIONS_EXCEEDED", used, max }) is preserved by catching MaxConnectionsExceededError and translating it; all other errors re-throw so non-quota failures (e.g. the ca_external_uidx unique violation on a same-token double submit) surface unchanged through app.onError.

Testing

  • Added channel-accounts.quota.integration.test.ts, a route-level test driving the real app (auth + tenant middleware) against a live Postgres with the Telegram Bot API mocked. It covers: the concurrent distinct-token race (exactly one 201 + one 402, slot count stays at the ceiling), the sequential happy-path/402 contract, a cross-channel Telegram-vs-WhatsApp-spawn race (shared lock → exactly one claim), reconnect of an existing error account (no new slot), a webhook-configuration failure (502, counted error row), and a same-token double submit (unique violation surfaces as 500, not swallowed as 402).
  • Verified the race test has teeth: with the route fix stashed (reverted to the buggy code) the race test fails — both concurrent requests return 201 and the slot count goes over; with the fix restored it passes.
  • Existing connection-quota.integration.test.ts (counting semantics) and channel-accounts.integration.test.ts (listing filter) still pass — no regression.
  • Routine checks — bun run lint, bun run typecheck, bun run test (unit suites), and bun run --filter @wateaminbox/api build — all pass. The new test's module mock is gated on RUN_DB_INTEGRATION=1 so it does not pollute the single-process unit run.
  • Connection/quota/spine integration suites (connection-admission-*, whatsapp/reconnect-archived, multi-connection, connection-purge, channel-account-purge, channel-spine-readiness, channel-spine-reconciler, and the packages/database channel-spine migration/schema tests) were run individually against live Postgres/NATS/Centrifugo/Meilisearch and pass.

Not verified

The full bun run test:integration:ts discovery runner could not complete in this environment: it aborts (set -e) on MinIO-dependent media/e2e suites upstream of this change, and MinIO's image could not be pulled here (pull access denied for minio/minio). The connection/quota tests relevant to this change do not require MinIO and were each verified individually with the runner's exact environment instead.


Automatic Fixes PRs can be configured here.

@detail-app
detail-app Bot requested a review from setkyar September 11, 2026 21:57
@setkyar
setkyar force-pushed the detail/bug-fix/fix-api-serialize-the-telegram-connect-quota-check-f47f5a branch from 3d0f06f to 9852111 Compare September 12, 2026 14:03
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.

[Detail Bug] Connection quota: Concurrent Telegram bot connects can exceed plan connection slot limit

1 participant