fix(api): serialize the Telegram connect quota check under the company advisory lock - #384
Open
detail-app[bot] wants to merge 1 commit into
Conversation
setkyar
force-pushed
the
detail/bug-fix/fix-api-serialize-the-telegram-connect-quota-check-f47f5a
branch
from
September 12, 2026 14:03
3d0f06f to
9852111
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 WhatsAppspawnConnectionpath correctly serialized the same count-then-insert withpg_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 passused < max, and both insert, leaving the workspace above its plan limit. Over-limitchannel_accountsrows persisted and kept counting towardcountUsedConnectionSlotsuntil manually archived. Introduced when the unifiedcountUsedConnectionSlotsquota 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 thegetMaxConnections+countUsedConnectionSlotscheck inside the insert transaction and acquire the same company-scoped advisory lockspawnConnectionuses (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 existing402response contract ({ error, code: "MAX_CONNECTIONS_EXCEEDED", used, max }) is preserved by catchingMaxConnectionsExceededErrorand translating it; all other errors re-throw so non-quota failures (e.g. theca_external_uidxunique violation on a same-token double submit) surface unchanged throughapp.onError.Testing
channel-accounts.quota.integration.test.ts, a route-level test driving the realapp(auth + tenant middleware) against a live Postgres with the Telegram Bot API mocked. It covers: the concurrent distinct-token race (exactly one201+ one402, 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 existingerroraccount (no new slot), a webhook-configuration failure (502, countederrorrow), and a same-token double submit (unique violation surfaces as 500, not swallowed as 402).201and the slot count goes over; with the fix restored it passes.connection-quota.integration.test.ts(counting semantics) andchannel-accounts.integration.test.ts(listing filter) still pass — no regression.bun run lint,bun run typecheck,bun run test(unit suites), andbun run --filter @wateaminbox/api build— all pass. The new test's module mock is gated onRUN_DB_INTEGRATION=1so it does not pollute the single-process unit run.connection-admission-*,whatsapp/reconnect-archived,multi-connection,connection-purge,channel-account-purge,channel-spine-readiness,channel-spine-reconciler, and thepackages/databasechannel-spine migration/schema tests) were run individually against live Postgres/NATS/Centrifugo/Meilisearch and pass.Not verified
The full
bun run test:integration:tsdiscovery 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.