Skip to content

fix(rpc): align funnel cache invalidation key and re-enable caching - #636

Open
FindMalek wants to merge 3 commits into
databuddy-analytics:stagingfrom
FindMalek:fix/funnels-cache-invalidation
Open

fix(rpc): align funnel cache invalidation key and re-enable caching#636
FindMalek wants to merge 3 commits into
databuddy-analytics:stagingfrom
FindMalek:fix/funnels-cache-invalidation

Conversation

@FindMalek

@FindMalek FindMalek commented Aug 20, 2026

Copy link
Copy Markdown

Fixes #634

Summary

All three funnel read paths in packages/rpc/src/routers/funnels.ts (list, getById, analyticsByLink) had caching explicitly disabled via disabled: true, // TODO: Remove this once we have a way to invalidate the cache. That TODO was stale — invalidateFunnelsCache already exists and is already called on create/update/delete — but re-enabling it blindly would have been wrong: getById cached under byId:${id} while invalidateFunnelsCache invalidated byId:${funnelId}:${websiteId}, so the two never matched and invalidation silently deleted a key that was never written.

Approach

  • packages/rpc/src/lib/funnels-cache.ts: changed the invalidation key from byId:${funnelId}:${websiteId} to byId:${funnelId}, matching what getById actually caches under (websiteId isn't known at read time — it's resolved from the row after the query runs).
  • packages/rpc/src/routers/funnels.ts: removed disabled: true from all three read paths now that the key mismatch is fixed. list's key already matched the invalidation format, and analyticsByLink is tag-based (funnel:${funnelId}) and already matched too — only getById had the actual bug.
  • Added packages/rpc/src/lib/funnels-cache.test.ts: caches a read, invalidates, and asserts the next read re-queries instead of returning the stale value. This test fails against the pre-fix code (confirmed locally by reverting the fix) and passes after it.

Verification

cd packages/rpc && bun test
# 314 pass, 0 fail

cd packages/rpc && bun run check-types
# clean

bunx ultracite check packages/rpc/src/lib/funnels-cache.ts packages/rpc/src/routers/funnels.ts
# Checked 2 files. No fixes applied.

Full monorepo turbo run check-types and turbo run test also ran clean via the repo's pre-commit/pre-push hooks.

Disclosure

I used Claude Code to trace the key mismatch (reading packages/redis/drizzle-cache.ts's invalidateByKey to confirm key formats must match exactly) and to write the regression test. I read every changed line, ran the fix locally, verified the test fails on the original code and passes on the fix, and ran the full test suite and type-check myself before opening this.


Summary by cubic

Aligns funnel cache invalidation keys with writes and re-enables caching for funnel reads, while enforcing authorization on every request. Previously, getById cached under byId:<funnelId> but invalidation targeted byId:<funnelId>:<websiteId>, and an auth check inside the cached query was skipped on cache hits.

  • packages/rpc/src/lib/funnels-cache.ts: invalidate byId:<funnelId> and list:<websiteId>.
  • packages/rpc/src/routers/funnels.ts: re-enable caching for list, getById, and analyticsByLink; resolve the owning websiteId and call withWorkspace before using the cache so authorization always runs; keep analyticsByLink tagged as funnel:<funnelId>.
  • packages/rpc/src/lib/funnels-cache.test.ts: add regression tests for getById and list; include the test in packages/rpc/package.json’s test script.
  • No API changes or migrations; expect correct invalidation, enforced permissions, and reduced DB load.

Written for commit 17a9908. Summary will update on new commits.

Review in cubic

invalidateFunnelsCache invalidated `byId:<funnelId>:<websiteId>`, but the
getById handler cached under `byId:<funnelId>` — the keys never matched, so
invalidation silently deleted a Redis key that was never written. All three
funnel read paths (list, getById, analyticsByLink) had caching disabled
entirely to work around this, even though list and analyticsByLink already
used a key/tag format the invalidation helper handles correctly.

Fixes databuddy-analytics#634
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

@FindMalek is attempting to deploy a commit to the Databuddy OSS Team on Vercel.

A member of the Team first needs to authorize it.

@vercel
vercel Bot temporarily deployed to Preview – documentation August 20, 2026 17:24 Inactive
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
documentation Skipped Skipped Aug 21, 2026 2:28pm

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2552233e-5dbb-4e3d-8bd2-dc81cc865a6f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR aligns funnel cache invalidation keys and safely re-enables caching while ensuring workspace authorization runs before cached getById reads.

  • Changes getById invalidation to target the key actually written.
  • Authorizes the funnel’s owning workspace before cache access.
  • Re-enables caching for funnel list, detail, and link-analytics reads.
  • Adds cache invalidation regression coverage to the standard RPC test command.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; both previously reported issues are fixed by authorizing before cache access and selecting the regression test in the standard RPC test task.

Important Files Changed

Filename Overview
packages/rpc/src/routers/funnels.ts Re-enables funnel caching and correctly moves workspace authorization ahead of cache access so cache hits remain authorized.
packages/rpc/src/lib/funnels-cache.ts Aligns detail-cache invalidation with the byId:<funnelId> key written by the router.
packages/rpc/src/lib/funnels-cache.test.ts Verifies that detail and list invalidation force subsequent reads to execute their query functions.
packages/rpc/package.json Adds the new funnel cache regression suite to the RPC package’s standard test command.

Sequence Diagram

sequenceDiagram
  participant Caller
  participant RPC as Funnel RPC
  participant DB
  participant Auth as Workspace Authorization
  participant Cache
  Caller->>RPC: getById(funnelId)
  RPC->>DB: Resolve active funnel websiteId
  DB-->>RPC: websiteId
  RPC->>Auth: Check read permission
  Auth-->>RPC: Authorized
  RPC->>Cache: Read byId:funnelId
  alt Cache hit
    Cache-->>RPC: Cached funnel
  else Cache miss
    RPC->>DB: Load funnel
    DB-->>RPC: Funnel
    RPC->>Cache: Store funnel
  end
  RPC-->>Caller: Funnel
Loading

Reviews (3): Last reviewed commit: "fix(rpc): authorize funnel getById befor..." | Re-trigger Greptile

Comment thread packages/rpc/src/routers/funnels.ts Outdated
@@ -226,7 +225,6 @@ export const funnelsRouter = {
.handler(({ context, input }) =>
cache.withCache({

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.

P1 security Cache hits bypass workspace authorization

When an authenticated principal requests a funnel previously cached by an authorized caller, withCache returns the shared byId:<funnelId> value without running the queryFn, so the only withWorkspace check is skipped and another tenant's complete funnel definition is disclosed.

How this was verified: The shared cache returns hits before invoking the query function that contains the route's only workspace permission check.

Knowledge Base Used: RPC package (@databuddy/rpc)

@@ -0,0 +1,111 @@
import { beforeEach, describe, expect, it, mock } from "bun:test";

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.

P2 Regression test is not selected

The RPC package's configured test command does not select src/lib/funnels-cache.test.ts or the complete src/lib directory, so this regression coverage is skipped by the standard package and CI test task.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Greptile flagged that re-enabling the getById cache let a workspace
authorization check that lived inside queryFn get skipped on a cache hit:
withCache returns the cached row directly without ever invoking queryFn,
so a second caller without access to the funnel's website could read it
once someone else had populated the cache.

Resolve the owning websiteId and call withWorkspace before touching the
cache, mirroring the same two-step pattern already used in update/delete
in this file. Authorization now runs on every request regardless of
cache state; only the row fetch itself is cached.

Also add the new funnels-cache.test.ts to the package's configured test
script (packages/rpc/package.json) — it was passing locally only because
`bun test` with no arguments picks up every *.test.ts file, but the
package's actual `test` script lists files explicitly and was silently
skipping it.
@FindMalek

Copy link
Copy Markdown
Author

Good catch — fixed in 9c1e36e.

getById's withWorkspace check lived inside queryFn, which withCache skips entirely on a cache hit, so a second caller without access to the funnel's website could have read a row someone else had already cached. Moved the websiteId lookup + withWorkspace call ahead of cache.withCache, mirroring the exact two-step pattern this file already uses in update/delete. Authorization now runs on every request regardless of cache state; only the row fetch itself is cached.

Also fixed: funnels-cache.test.ts was passing locally only because a bare bun test picks up every *.test.ts file — the package's actual test script (packages/rpc/package.json) lists files explicitly and was silently skipping it. Added it there so it's actually enforced.

@vercel
vercel Bot temporarily deployed to Preview – documentation August 20, 2026 17:31 Inactive
@FindMalek

Copy link
Copy Markdown
Author

@greptile review

@izadoesdev
izadoesdev deleted the branch databuddy-analytics:staging August 21, 2026 09:05
@izadoesdev izadoesdev closed this Aug 21, 2026
@izadoesdev izadoesdev reopened this Aug 21, 2026
@vercel
vercel Bot temporarily deployed to Preview – documentation August 21, 2026 14:28 Inactive
@FindMalek

Copy link
Copy Markdown
Author

@izadoesdev this is ready for review whenever you have a chance — CI is green aside from the Vercel preview checks, which need a team member to authorize the deploy (outside my permissions as an external contributor).

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.

2 participants