diff --git a/AGENTS.md b/AGENTS.md index 49fdf1c450..4483083cb7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -624,10 +624,10 @@ When backend code uses the plugin read-path (`/updates`, `/stats`, `/channel_sel - Logical replication replicates **table data**, not derived objects like **views** and **SQL functions**. - Treat the read replica (what you see in PlanetScale) as the source of truth for what is queryable from plugin endpoints. - Do **not** query credits ledger tables/views from the replica (e.g. `usage_credit_*` / `usage_credit_balances`). If plugin logic needs a “has credits” signal, **materialize it into a replicated column/table** (example: an org-level boolean flag that is refreshed by primary-side jobs). -- `/updates`, `/stats`, and `/channel_self` are extremely hot paths and can be called hundreds of times per second. -- Those endpoints must not call the primary Supabase/Postgres database in-request or through `backgroundTask()` side effects unless there is no other practical option. -- Background work is not an exception: do not enqueue primary-DB RPCs, writes, or lookups from these plugin endpoints just because the response is returned first. -- If an unavoidable primary write remains for one of these endpoints, keep it minimal, document the reason inline, and treat it as an exception that requires extra review. +`/updates`, `/stats`, and `/channel_self` are extremely hot paths and can be called hundreds of times per second, so they must never add direct primary-DB work, service-role RPCs, queue writes, or any other Postgres side effect from the live request path. +Those endpoints must not call the primary Supabase/Postgres database in-request or through `backgroundTask()` side effects unless there is no other practical option. +Background work is not an exception: do not enqueue primary-DB RPCs, writes, or lookups from these plugin endpoints just because the response is returned first. +If a feature seems to require direct DB work from these endpoints, stop and design an alternative that keeps the request path replica-safe and off the primary DB; treat such request-path side effects as forbidden unless explicitly approved, and document any unavoidable primary writes inline as an exception that needs extra review. ## Pull Request Guidelines diff --git a/tests/process-cron-stats-jobs.test.ts b/tests/process-cron-stats-jobs.test.ts index eb1cd8e5ce..8ead60dcec 100644 --- a/tests/process-cron-stats-jobs.test.ts +++ b/tests/process-cron-stats-jobs.test.ts @@ -66,4 +66,40 @@ describe('cron_stat_app queue resilience', () => { expect(queuedMessages[0]?.message?.payload?.orgId).toBe(ORG_ID_CRON_QUEUE) expect(queuedMessages[0]?.message?.payload?.todayOnly).toBe(false) }) + + it.concurrent('live /stats traffic does not enqueue cron_stat_app directly', async () => { + await clearCronStatAppMessages(liveQueueAppId) + + const versionName = '1.0.0' + await createAppVersions(versionName, liveQueueAppId) + + const baseData = getBaseData(liveQueueAppId) + const payload = { + ...baseData, + action: 'set', + device_id: randomUUID().toLowerCase(), + version_build: versionName, + version_name: versionName, + } + + const firstResponse = await fetch(getEndpointUrl('/stats'), { + method: 'POST', + headers, + body: JSON.stringify(payload), + }) + expect(firstResponse.status).toBe(200) + + const secondResponse = await fetch(getEndpointUrl('/stats'), { + method: 'POST', + headers, + body: JSON.stringify({ + ...payload, + device_id: randomUUID().toLowerCase(), + }), + }) + expect(secondResponse.status).toBe(200) + + const queuedMessages = await getCronStatAppMessages(liveQueueAppId) + expect(queuedMessages).toHaveLength(0) + }) })