Skip to content

feat(updates): colo cache for /updates with targeted worldwide invalidation#2662

Open
riderx wants to merge 12 commits into
mainfrom
feat/updates-colo-cache
Open

feat(updates): colo cache for /updates with targeted worldwide invalidation#2662
riderx wants to merge 12 commits into
mainfrom
feat/updates-colo-cache

Conversation

@riderx

@riderx riderx commented Jul 10, 2026

Copy link
Copy Markdown
Member

What

Caches the app-level part of every update check in the colo cache (Cache API — free) and adds a dedicated invalidation fan-out that bumps each regional plugin worker directly, so a database change is reflected worldwide in ~1s. Supersedes the read-replica PRs #2640 / #2619 (closed): at 83M requests/day, any per-touch-billed read path (DO duration, KV, D1) loses to a free cache; this keeps drizzle and today's Hyperdrive/replica origin untouched.

How it works

Read path (update.ts, gated by UPDATES_CACHE_MODE, default off):

  • cachedGetAppOwner + cachedRequestInfos are drop-in replacements for the existing pg functions. The app-level payloads (app owner + plan validation, channel resolution incl. rollout fields, manifest per version) are cached per (app, platform, defaultChannel, flags) under a per-app version token, TTL 60s backstop.
  • Per-device semantics unchanged: semver compares and rollout decisions run per request in the worker; devices of apps with channel_device_count > 0 keep their per-device override lookup on every request (per-device query params — Hyperdrive can't cache those today either). Customers with one default channel and no overrides get the zero-query fast path; the counter inside the cached payload flips the override path back on.
  • Negative results (unknown app) cached too; apps INSERT trigger clears them instantly.

Invalidation (the "even faster than TTL" part):

DB commit → trigger (channels/channel_devices/apps/app_versions/orgs/stripe_info)
  → pg_net POST triggers/cache_invalidate (async, ms)
  → fan-out: one POST per regional plugin domain (PLUGIN_INVALIDATE_URLS)
  → each worker is placement-pinned: the call lands in the colo serving that
    region and bumps the app's token → every cached variant unreachable at once

Auth: fan-out uses the existing API secret; the plugin route uses a dedicated CACHE_INVALIDATE_SECRET. Every failure path is soft — the 60s TTL is the backstop, and with the flag off nothing changes at all.

Why this beats the replica attempts

cost @83M/day freshness drizzle
DO-per-app replicas (#2640) $1.4k–7k/mo (duration billing) 5s no (SQLite mirror)
KV / D1 $1.2k+/mo / throughput ceiling 60s no
colo cache + invalidation ~$0 (Cache API is free) ~1s on change, 60s backstop yes — same pg.ts queries on miss

Misses still go through Hyperdrive → existing replicas; with ~95%+ of reads served from cache the replica fleet can shrink afterwards.

Rollout

  1. Apply the migration (trigger fn + 6 triggers, dry-run applied+rolled back on local Supabase).
  2. Add CACHE_INVALIDATE_SECRET + PLUGIN_INVALIDATE_URLS (CSV of the 9 regional plugin domains) to internal/cloudflare/.env.prod, deploy secrets to api + plugin workers.
  3. Flip UPDATES_CACHE_MODE: "on" per regional plugin env, watch hit rate via updates cache hit logs and replica QPS.

Tested

  • 16 new unit tests (tests/updates-colo-cache.unit.test.ts): gating, owner caching incl. negative results, token-bump invalidation, channel caching with per-device override always uncached, override skip at count 0, null-channel caching, per-platform/channel key separation, rollout decided per device on a cached row, route auth (401/503/400) + token bumps, fan-out per region with secret, soft-skip on missing env, URL parsing.
  • Full unit sweep: 970 passed. Typecheck, oxlint, wrangler dry-run bundles (plugin + api), migration dry-run.

🤖 Generated with Claude Code


Note

High Risk
Changes the high-traffic update-check path and correctness depends on cache + worldwide invalidation; misconfiguration or missed fan-out could serve stale channel/version data until TTL, though the flag defaults off and TTL is the backstop.

Overview
Adds an optional colo-local Cache API layer for the /updates hot path (UPDATES_CACHE_MODE, default off), with ~60s TTL and per-app version tokens so one bump invalidates all cached variants for that app.

When enabled, update.ts uses cachedGetAppOwner and cachedRequestInfos instead of direct Postgres reads for app-level data (owner/plan, channel resolution, rollout manifests); per-device override lookups and rollout decisions still run every request. pg.ts is refactored to share requestChannelOverrideLookup, queryAppOwnerPostgres, and rollout resolution for the cached and uncached paths.

Invalidation: a migration adds statement-level triggers on channels, channel_devices, apps, app_versions, manifest, orgs, and stripe_info → notify_updates_cache_invalidation (pg_net) → triggers/cache_invalidate on the API worker, which fans out POSTs to every URL in PLUGIN_INVALIDATE_URLS → regional plugin /cache_invalidate bumps tokens in each colo. Prod API wrangler sets the nine regional plugin domains; each plugin env gets UPDATES_CACHE_MODE: "off" until flipped per region.

Unit and Postgres integration tests cover cache semantics, auth, chunking, and bulk trigger aggregation.

Reviewed by Cursor Bugbot for commit 2d73154. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added colo-local caching for /updates, gated by deployment configuration, with per-app token invalidation and configurable TTLs.
    • Added regional cache-invalidation fan-out via a new authenticated /cache_invalidate endpoint.
    • Switched updates invalidation to statement-level database triggers that batch and notify regional invalidation workers.
    • Added configuration for PLUGIN_INVALIDATE_URLS and UPDATES_CACHE_MODE.
  • Bug Fixes

    • Prevents transient lookup failures from being cached as “unknown” values.
  • Tests

    • Added end-to-end unit and integration coverage for caching, validation, authentication, batching, and regional fan-out.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a regional Colo cache for /updates, token-based invalidation, database triggers for affected app data, authenticated fanout endpoints, cache-aware update retrieval, environment configuration, and test coverage.

Changes

Updates Colo Cache

Layer / File(s) Summary
Cache contracts and storage
supabase/functions/_backend/utils/cloudflare.ts, supabase/functions/_backend/utils/pg.ts, supabase/functions/_backend/utils/updates_colo_cache.ts
Adds cache bindings, token storage, cached owner/channel/manifest lookups, manifest loader injection, and shared channel-override resolution.
Updates request integration
supabase/functions/_backend/utils/update.ts, supabase/functions/_backend/utils/pg.ts
Selects cached or Postgres-backed owner and request-info retrieval according to UPDATES_CACHE_MODE.
Invalidation handlers and routing
supabase/functions/_backend/private/cache_invalidate.ts, supabase/functions/_backend/triggers/cache_invalidate.ts, supabase/functions/triggers/index.ts, cloudflare_workers/api/index.ts, cloudflare_workers/plugin/index.ts, cloudflare_workers/*/wrangler.jsonc
Adds authenticated token invalidation, regional chunked fanout, HTTP route registrations, and deployment configuration.
Database invalidation triggers
supabase/migrations/20260711100000_updates_cache_invalidation.sql
Adds authenticated notification functions and statement-level triggers for app, channel, version, manifest, organization, and Stripe data changes.
Cache and invalidation validation
tests/updates-colo-cache.unit.test.ts, tests/updates-cache-invalidation.test.ts
Covers cache behavior, token bumps, endpoint authentication, regional fanout, batching, trigger wiring, and permissions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Database
  participant SupabaseTrigger
  participant RegionalPlugin
  participant ColoCache
  Database->>SupabaseTrigger: Notify app_ids after data changes
  SupabaseTrigger->>RegionalPlugin: POST /cache_invalidate with chunks
  RegionalPlugin->>ColoCache: Bump app cache tokens
  ColoCache-->>RegionalPlugin: Return bump count
  RegionalPlugin-->>SupabaseTrigger: Return regional success count
Loading

Possibly related PRs

  • Cap-go/capgo#2053: Both changes modify /updates retrieval flow in update.ts and related request-info handling.

Suggested labels: codex

Suggested reviewers: Dalanir, WcaleNieWolny, RobinWitch

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.04% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding colo cache with worldwide invalidation for /updates.
Description check ✅ Passed The description covers the summary, implementation, rollout, and testing, so it is mostly complete despite not matching the template exactly.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

@codspeed-hq

codspeed-hq Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 43 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing feat/updates-colo-cache (2d73154) with main (92b9b67)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Comment thread supabase/migrations/20260711100000_updates_cache_invalidation.sql Outdated
Comment thread supabase/functions/_backend/private/cache_invalidate.ts Outdated
Comment thread supabase/migrations/20260711100000_updates_cache_invalidation.sql Outdated
Comment thread supabase/functions/_backend/utils/updates_colo_cache.ts
@cursor cursor Bot requested review from Dalanir and WcaleNieWolny July 10, 2026 17:17

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Cursor Bugbot completed with status skipping, so the required automated review did not finish successfully. This /updates colo-cache and worldwide invalidation change exceeds the low-risk approval threshold; human review is required.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot completed with status skipping and reported 3 unresolved medium-severity findings on the /updates colo-cache path, and this change exceeds the low-risk approval threshold (hot-path caching, migration, worldwide invalidation). Human review is required; reviewers assigned.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Cursor Bugbot finished with a skipped check and reported 3 potential issues on this /updates colo-cache change, so I am not approving yet. Requested human reviewers for the hot-path cache and invalidation work.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. I am not approving because Cursor Bugbot finished with status skipping and reported 3 unresolved findings on this /updates colo-cache change. WcaleNieWolny and Dalanir are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@riderx

riderx commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Review round 1 fixed in e95cebf:

Sonar duplication gate (7.5% > 3%) — the cached path now shares requestChannelOverrideLookup and resolveRolloutChannelDataPostgres (with an injectable manifest loader) with the direct path instead of duplicating them.

Cursor — unbounded org fan-out: the trigger notify helper dedupes, caps at 1000 apps and chunks every pg_net body; the fan-out posts ≤100 ids per request per region.

Cursor — silent 100-id truncation: the plugin route now rejects oversized lists with an explicit 400 (too_many_app_ids); nothing is ever silently dropped since the fan-out chunks to the cap.

Cursor — manifest staleness: manifest entries are now cached under the app's version token (a bump invalidates them together with channel payloads), and new statement-level triggers on manifest (INSERT/DELETE via transition tables) bump the token on manifest-only changes — one notification per statement, not per row, so bundle uploads with hundreds of files cost one pg_net call.

Tests grew to 20 (chunking, loud cap, manifest-bump invalidation); full sweep 974 green; migration re-applied+rolled back on local Supabase; typos/lint/typecheck clean.

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot added the codex label Jul 10, 2026
Comment thread supabase/migrations/20260711100000_updates_cache_invalidation.sql

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot did not reach a terminal state within the 8-minute window (still pending on the latest push), and this /updates colo-cache plus worldwide invalidation change exceeds the medium-risk approval threshold. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@riderx

riderx commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Security round fixed:

Cursor — SECURITY DEFINER function RPC-callable by API roles: notify_updates_cache_invalidation (and both trigger functions) now REVOKE ALL FROM PUBLIC, anon, authenticated, GRANT EXECUTE TO service_role only. Verified on local Supabase: has_function_privilege returns false/false/true for anon/authenticated/service_role.

The other findings in this round (rollout manifest cache ignoring the token, missing manifest triggers) were posted against the previous head and are already fixed in e95cebf (token-scoped manifest cache + statement-level manifest triggers).

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@supabase/functions/_backend/private/cache_invalidate.ts`:
- Around line 40-44: Update the loop in the cache invalidation handler to invoke
bumpAppCacheToken concurrently for all appIds, await the resulting promises
together, and count only successful bumps while preserving the existing bumped
total behavior.

In `@supabase/functions/_backend/triggers/cache_invalidate.ts`:
- Around line 17-19: Replace the duplicated FANOUT_CHUNK_SIZE literal in the
cache invalidation trigger with an import of the shared MAX_INVALIDATE_APPS
constant from private/cache_invalidate.ts, and use that imported symbol wherever
the fanout chunk size is needed. Remove the comment-only synchronization
contract and ensure the existing fanout behavior remains unchanged.
- Around line 48-52: Define PLUGIN_INVALIDATE_URLS and CACHE_INVALIDATE_SECRET
in cloudflare_workers/api/wrangler.jsonc using the project’s expected
configuration or secret mechanism, so cache invalidation can run on the API
worker and the checks in the cache invalidation trigger no longer silently skip.

In `@supabase/migrations/20260711100000_updates_cache_invalidation.sql`:
- Around line 1-163: Add Postgres-level integration tests for the migration’s
functions notify_updates_cache_invalidation, invalidate_updates_cache, and
invalidate_updates_cache_manifest, exercising deduplication, chunking/capping,
table-operation branching, and affected app resolution. Verify each trigger on
channels, channel_devices, apps, app_versions, manifest, orgs, and stripe_info
invokes invalidation with the correct app IDs, including statement-level
manifest behavior; use real database SQL rather than the mocked pg.ts unit test.
- Around line 12-51: Lock down the SECURITY DEFINER function
public.notify_updates_cache_invalidation by revoking EXECUTE from PUBLIC and
granting EXECUTE only to the intended internal role(s). Add the privilege
statements after the function definition, preserving access for trusted callers
while preventing arbitrary RPC invocation and pg_net fan-out.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: debd1354-1a2e-4485-bab5-079aa365883b

📥 Commits

Reviewing files that changed from the base of the PR and between 5003de2 and e95cebf.

📒 Files selected for processing (12)
  • cloudflare_workers/api/index.ts
  • cloudflare_workers/plugin/index.ts
  • cloudflare_workers/plugin/wrangler.jsonc
  • supabase/functions/_backend/private/cache_invalidate.ts
  • supabase/functions/_backend/triggers/cache_invalidate.ts
  • supabase/functions/_backend/utils/cloudflare.ts
  • supabase/functions/_backend/utils/pg.ts
  • supabase/functions/_backend/utils/update.ts
  • supabase/functions/_backend/utils/updates_colo_cache.ts
  • supabase/functions/triggers/index.ts
  • supabase/migrations/20260711100000_updates_cache_invalidation.sql
  • tests/updates-colo-cache.unit.test.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • Cap-go/capacitor-updater (manual)

Comment thread supabase/functions/_backend/private/cache_invalidate.ts Outdated
Comment thread supabase/functions/_backend/triggers/cache_invalidate.ts Outdated
Comment thread supabase/functions/_backend/triggers/cache_invalidate.ts Outdated
Comment thread supabase/migrations/20260711100000_updates_cache_invalidation.sql Outdated
Comment thread supabase/migrations/20260711100000_updates_cache_invalidation.sql

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fe0102d. Configure here.

Comment thread supabase/functions/_backend/utils/update.ts
Comment thread supabase/functions/_backend/utils/updates_colo_cache.ts
Comment thread supabase/migrations/20260711100000_updates_cache_invalidation.sql Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with status skipping and reported 3 unresolved findings on the latest commit, including a high-severity cache-skew issue. WcaleNieWolny and Dalanir are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@riderx

riderx commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

Round 3 fixed:

Cursor — owner/channel cache skew (High): token reads are now memoized per request, so one update check always reads a single invalidation generation — a bump landing mid-request can't mix stale and fresh payloads in one response. (The second half of that finding — "old entries aren't deleted, delaying invalidation" — is not accurate: a bump changes the token embedded in every payload key, making old entries unreachable immediately; they expire unread.)

Cursor — DB errors cached as missing: cachedGetAppOwner now uses a throwing queryAppOwnerPostgres variant; transient query failures keep today's behavior (null owner for that request) but are never cached, so a replica blip can't classify an app as on-prem for the TTL.

Cursor — stripe_info deletes skip invalidation: trigger extended to DELETE with TG_OP-safe customer resolution.

Also from CodeRabbit's round: token bumps parallelized, FANOUT_CHUNK_SIZE imports MAX_INVALIDATE_APPS, PLUGIN_INVALIDATE_URLS provisioned on the api worker prod env with the real regional domains, and Postgres-level tests added (tests/updates-cache-invalidation.test.ts, 12 tests against local Supabase: trigger wiring on all tables, statement-level manifest triggers, notify chunking/cap/dedupe through the pg_net queue, privilege lockdown, and a functional insert asserting queued invalidations carry the app_id).

Unit tests now 22 on the cache module; full sweep 976 green; migration re-applied on local Supabase (re-runnable).

🤖 Generated with Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot did not reach a terminal state within the 8-minute window on the latest push, and this /updates colo-cache plus worldwide invalidation change exceeds the medium-risk approval threshold. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: this /updates colo-cache and worldwide invalidation change exceeds the low-risk approval threshold despite Cursor Bugbot passing cleanly on the latest commit. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@riderx

riderx commented Jul 10, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.


You're currently rate limited under our Fair Usage Limits Policy. Your recent PR review activity is in the 95th percentile or higher among CodeRabbit users, so adaptive limits apply. Your next review will be available in 8 minutes.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 13f4829. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 13f4829. This /updates colo-cache change exceeds the low-risk approval threshold; Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_238a5d39-1062-4f86-b7d3-934e404d9532)

@riderx

riderx commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

Latest round handled in 490978a:

Fixed

  • Sonar reliability gate: localeCompare comparator for cache-key params, resolveRolloutChannelDataPostgres now takes an options object, redundant block removed in queryAppOwnerPostgres.
  • ALTER FUNCTION … OWNER TO postgres on both invalidation functions (repo convention, keeps SECURITY DEFINER consistent regardless of migration role).
  • Empty-input notify test observes total endpoint queue growth in a transaction (the marker filter could never match, as flagged).
  • Fan-out chunk test asserts each region receives the complete deduplicated 150-app set, not just the global union.

Not changing, with reasons

  • "Use the dedicated invalidation secret instead of API_SECRET" — the dedicated CACHE_INVALIDATE_SECRET existed in an earlier revision and was removed by explicit owner decision (@riderx): one intercommunication secret, one less thing to provision and rotate. middlewareAPISecret is timing-safe and gates the same class of internal endpoints (all triggers/* routes); a holder of that secret can already do far more damaging things than bump cache tokens. The file-header comment now states this explicitly.
  • "Use createHono for these routes" ×2 — every sibling route module (plugins/updates.ts, triggers/on_app_update.ts, private/latency.ts, …) constructs new Hono<MiddlewareKeyVariables>() and inherits request-id/logging middleware from the parent app built with createHono in each worker/function entry. These two routes are mounted the same way; using the factory per-submodule would double-install middleware and diverge from the codebase pattern.

🤖 Generated with Claude Code

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 490978a. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 490978a. This /updates colo-cache change exceeds the low-risk approval threshold; Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

riderx and others added 11 commits July 11, 2026 12:38
…dation

Most apps resolve updates identically for every device on the same
(platform, defaultChannel): serve them from the colo cache (Cache API,
free) with zero database queries, while per-device work (semver compares,
rollout decisions, override lookups) stays per-request.

- updates_colo_cache: app-owner + channel resolution cached under a
  per-app version token, TTL backstop 60s; apps with channel_device_count
  > 0 keep their per-device override lookup on every request, so a
  customer gaining a FIRST override reacts within one token bump;
  rollout manifest cached per version id, non-empty results only
- targeted invalidation: DB triggers (channels, channel_devices, apps,
  app_versions, orgs, stripe_info) call triggers/cache_invalidate via
  pg_net, which fans one POST out to every regional plugin worker —
  placement-pinned, so each call lands in the colo whose cache serves
  that region; end-to-end ~1s from commit, TTL as backstop
- /cache_invalidate route on plugin workers guarded by a dedicated
  shared secret; fanout guarded by the existing API secret
- gated by UPDATES_CACHE_MODE (default off) per regional env

At 83M requests/day this removes ~95%+ of read queries from the replica
fleet; enables shrinking it once hit rates are confirmed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review round 1 (Sonar duplication gate + Cursor findings):

- share requestChannelOverrideLookup and resolveRolloutChannelDataPostgres
  between the direct and cached read paths (injectable manifest loader)
  instead of duplicating them in updates_colo_cache
- manifest cache is now keyed under the app token, so a token bump also
  invalidates cached rollout manifests; new statement-level manifest
  triggers (INSERT/DELETE via transition tables) bump on manifest-only
  changes without one pg_net call per row
- invalidation payloads are bounded end to end: trigger helper dedupes,
  caps at 1000 apps and chunks pg_net bodies; the fan-out posts one chunk
  of <=100 per request; the plugin route rejects oversized lists loudly
  instead of silently truncating

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
notify_updates_cache_invalidation is SECURITY DEFINER and performs
privileged fan-out with get_apikey(): revoke default PUBLIC/anon/
authenticated execute so API roles cannot trigger invalidation storms;
grant service_role only. Trigger functions revoked the same way for
defense in depth. Triggers switched to CREATE OR REPLACE (repo
convention, re-runnable).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- new tests/updates-cache-invalidation.test.ts against local Supabase:
  row-trigger wiring on all 6 tables, statement-level manifest triggers
  with transition tables, notify chunking (150 ids = 100+20), 1000 cap,
  dedupe, empty-input no-op, privilege lockdown, and a functional
  channels/apps insert asserting pg_net queue rows carry the app_id
- parallelize token bumps in the plugin invalidate route (CodeRabbit)
- FANOUT_CHUNK_SIZE now imports MAX_INVALIDATE_APPS instead of mirroring
  it by comment (CodeRabbit)
- provision PLUGIN_INVALIDATE_URLS on the api worker prod env with the
  real regional plugin domains (CodeRabbit)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pe deletes

Review round 3 (Cursor):

- per-request token memo (WeakMap on the request): every cached lookup of
  one update check reads the same invalidation generation, so a token bump
  landing mid-request can no longer mix stale owner data with fresh
  channel data in a single response
- cachedGetAppOwner uses the new throwing queryAppOwnerPostgres variant:
  a transient replica/Hyperdrive failure keeps today's null-owner behavior
  but is never cached as "unknown app" (which would misclassify the app as
  on-prem for the whole TTL)
- stripe_info trigger now covers DELETE (orphaned-row cleanup bumps the
  cache too), with TG_OP-safe customer_id resolution

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
No reason for a dedicated CACHE_INVALIDATE_SECRET: the plugin
/cache_invalidate route now uses middlewareAPISecret (timing-safe, same
intercommunication secret already provisioned on every worker) and the
fan-out forwards the apisecret header it was authenticated with. One less
secret to deploy and rotate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ce types

- unknown apps (Capgo removed, misconfigured open-source installs) are the
  eternal hot path: cache the negative owner result for
  UPDATES_CACHE_NEGATIVE_TTL_SECONDS (default 600s) instead of the 60s
  payload TTL, so they cost at most one database query per colo per
  10 minutes; safe because the apps INSERT trigger bumps the token the
  moment an app is created
- add missing devices.country_code to both generated supabase.types.ts
  (backend + frontend): main's country_code feature (#2646/#2659) landed
  without the regen, breaking typecheck on every PR

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…replica-safe negatives

Review blockers:

- all invalidation triggers are now STATEMENT-level with transition
  tables: bulk writes (daily channel_devices cleanup deleting thousands
  of rows, bundle uploads, org backfills) produce one aggregated,
  deduplicated notification per statement instead of one pg_net call per
  row — bounded invalidation traffic even with cache mode off
- cache keys use a fixed synthetic origin instead of the inbound request
  URL: devices hitting alias hosts (*.usecapgo.com, updater.* zones,
  api.capgo.app/updates) now share one entry per colo with the canonical
  domain, so the fan-out token bump reaches them too
- negative (unknown-app) caching escalates instead of starting long: the
  first sightings of a missing app use the short payload TTL (a brand-new
  app may not be on the regional replica yet); only after 15 minutes of
  consecutive absence — beyond any plausible replication lag — does the
  600s negative TTL kick in
- oversized-invalidation unit test was passing on auth failure (stale
  header); it now authenticates properly and asserts the size guard
- pg-level tests rewritten for the statement triggers, including a bulk
  test: 500-row channel_devices delete = exactly one queued notification

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
a3e0ef7 moved the changelog window from the action input (from_tag:) to
an env var (FROM_TAG:) for the Cloudflare Workers AI generator but left
the assertion on the old string, breaking the Tinbase DB test job on
every branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- deterministic localeCompare comparator for cache-key param ordering
- resolveRolloutChannelDataPostgres takes an options object (8 params
  over the limit) and queryAppOwnerPostgres loses its redundant block
- ALTER FUNCTION ... OWNER TO postgres on both invalidation functions so
  SECURITY DEFINER privileges stay consistent regardless of the
  migration role (repo convention)
- empty-input notify test now observes total endpoint queue growth
  inside a transaction instead of a marker filter that could never match
- fan-out chunk test asserts every region receives the complete
  deduplicated set, not just the global union

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_79ef7817-3a54-4bfa-8067-6cd671644d6a)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 31bee67. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale comment

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 31bee67. This /updates colo-cache change exceeds the low-risk approval threshold; Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

Main shipped its own (more thorough) fix for the Workers AI changelog
assertions; drop the interim branch-side alignment in favor of it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_bbdcc9d8-cad9-4552-807f-9c59e86497d9)

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 2d73154. This /updates colo-cache change exceeds the low-risk approval threshold; Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver External

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: high. Not approving: Cursor Bugbot finished with status skipping on the latest push (usage limit reached) and has no clean review for commit 2d73154. Dalanir and WcaleNieWolny are already requested for human review.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Approver

@sonarqubecloud

Copy link
Copy Markdown

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant