feat: optimize hot database queries with multi-layer caching#19068
feat: optimize hot database queries with multi-layer caching#19068charlesBochet merged 23 commits intomainfrom
Conversation
Introduces CoreEntityCacheService for non-workspace-scoped entities (Workspace, User, UserWorkspace) and leverages WorkspaceCacheService for workspace-scoped entities (ApiKey, LogicFunction) to dramatically reduce database load on the authentication hot path. Key changes: - New CoreEntityCacheService with provider pattern (in-process + Redis) mirroring WorkspaceCacheService but keyed by entity primary key - Three CoreEntityCache providers: WorkspaceEntity, AuthContextUser, UserWorkspaceEntity - New apiKeyMap WorkspaceCache provider for API key lookups - JwtAuthStrategy refactored to use caches instead of direct DB queries - CronTriggerCronJob refactored to use flatLogicFunctionMaps cache - Cache invalidation hooks in WorkspaceService and ApiKeyService Expected impact: - WorkspaceEntity queries: 638/min → near zero (cached) - ApiKeyEntity queries: 491/min → near zero (cached per workspace) - UserEntity queries: 147/min → near zero (cached) - UserWorkspaceEntity queries: 143/min → near zero (cached) - LogicFunctionEntity queries: 1800/min → near zero (workspace cache) Made-with: Cursor
There was a problem hiding this comment.
Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.
packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
Pull request overview
Adds multi-layer caching to reduce high-frequency DB queries on the JWT auth hot path and cron-trigger logic execution, leveraging Redis-backed validation plus short in-process caching.
Changes:
- Introduces a new
CoreEntityCacheService(local + Redis) with provider/decorator pattern for core (non-workspace-scoped) entity lookups. - Adds a workspace-scoped
apiKeyMapcache and wires cache invalidation into API key mutations. - Refactors
JwtAuthStrategyandCronTriggerCronJobto use caches instead of direct repository queries.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts | Adds apiKeyMap to workspace cache key/type map. |
| packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts | Registers core-entity cache module/provider in workspace module. |
| packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts | Invalidates core-entity cache on workspace mutations/deletes. |
| packages/twenty-server/src/engine/core-modules/workspace/services/workspace-entity-cache-provider.service.ts | Adds cache provider for workspace lookups. |
| packages/twenty-server/src/engine/core-modules/user/user.module.ts | Registers user cache provider. |
| packages/twenty-server/src/engine/core-modules/user/services/user-entity-cache-provider.service.ts | Adds cache provider for AuthContextUser projection. |
| packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts | Registers user-workspace cache provider. |
| packages/twenty-server/src/engine/core-modules/user-workspace/services/user-workspace-entity-cache-provider.service.ts | Adds cache provider for user-workspace lookups. |
| packages/twenty-server/src/engine/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts | Switches cron logic-function lookup from DB to cached flat maps. |
| packages/twenty-server/src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum.ts | Adds EngineCoreEntity cache namespace. |
| packages/twenty-server/src/engine/core-modules/auth/token/token.module.ts | Imports CoreEntityCacheModule and removes unused ApiKeyEntity repo. |
| packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts | Replaces Workspace/User/UserWorkspace/ApiKey repository lookups with caches. |
| packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.spec.ts | Updates strategy tests to mock the new cache-based dependencies. |
| packages/twenty-server/src/engine/core-modules/auth/auth.module.ts | Imports CoreEntityCacheModule for auth module wiring. |
| packages/twenty-server/src/engine/core-modules/api-key/services/workspace-api-key-map-cache.service.ts | New workspace cache provider loading API keys into an ID-keyed map. |
| packages/twenty-server/src/engine/core-modules/api-key/services/api-key.service.ts | Invalidates/recomputes apiKeyMap cache on create/update/revoke. |
| packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts | Registers the new WorkspaceApiKeyMapCacheService. |
| packages/twenty-server/src/engine/core-entity-cache/types/core-entity-local-cache-entry.type.ts | Defines local-cache entry/version types for core-entity caching. |
| packages/twenty-server/src/engine/core-entity-cache/types/core-entity-cache-key.type.ts | Defines cache key names + type map for core entities. |
| packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts | Implements local+Redis core-entity caching with hash validation + memoization. |
| packages/twenty-server/src/engine/core-entity-cache/interfaces/core-entity-cache-provider.service.ts | Defines core-entity cache provider abstraction. |
| packages/twenty-server/src/engine/core-entity-cache/exceptions/core-entity-cache.exception.ts | Adds domain exception type for core-entity cache. |
| packages/twenty-server/src/engine/core-entity-cache/decorators/core-entity-cache.decorator.ts | Adds @CoreEntityCache provider registration decorator. |
| packages/twenty-server/src/engine/core-entity-cache/core-entity-cache.module.ts | Exposes core-entity cache service via a dedicated module. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts
Show resolved
Hide resolved
...ne/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts
Show resolved
Hide resolved
...wenty-server/src/engine/core-modules/api-key/services/workspace-api-key-map-cache.service.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-entity-cache/types/core-entity-cache-key.type.ts
Outdated
Show resolved
Hide resolved
The ApiKeyService now depends on WorkspaceCacheService for cache invalidation, so its test module needs to provide a mock. Made-with: Cursor
There was a problem hiding this comment.
3 issues found across 24 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts:92">
P1: Throwing `CoreEntityCacheException` for an invalid UUID in the auth hot path will surface as `INTERNAL_SERVER_ERROR` to clients, since this exception type is not mapped by auth exception handlers. When the entity ID comes from an untrusted JWT payload, a malformed token should result in an `UNAUTHENTICATED` error. Return `null` here instead (or catch and rethrow as `AuthException` in the caller) so the existing auth flow handles it gracefully.</violation>
<violation number="2" location="packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts:168">
P2: Race condition: `invalidate` is missing a second `memoizer.clearKeys` after `flush`. A concurrent `get()` can re-populate the memoizer with stale data between the first `clearKeys` and the `flush`, leaving stale values cached for up to 10 seconds. `invalidateAndRecompute` already guards against this with a trailing `clearKeys`—apply the same pattern here.</violation>
</file>
<file name="packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts:295">
P1: Do not let cache invalidation failure abort activation after setting `ONGOING_CREATION`; this can leave workspaces stuck in an unretryable state.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-entity-cache/services/core-entity-cache.service.ts
Show resolved
Hide resolved
|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:52203 This environment will automatically shut down after 5 hours. |
When users are soft-deleted or userWorkspaces are removed, the CoreEntityCacheService must be invalidated to prevent stale cache entries from allowing authentication to succeed for deleted entities. Also adds missing CoreEntityCacheService mock in workspace.service.spec. Made-with: Cursor
- Return null instead of throwing for invalid UUID in get() to prevent 500 errors when auth tokens contain malformed entity IDs - Add second memoizer.clearKeys after flush in invalidate() to prevent race condition where concurrent get() could repopulate stale data Made-with: Cursor
UserService now depends on CoreEntityCacheService for cache invalidation on user deletion, so the test module needs the mock provider. Made-with: Cursor
Replace raw TypeORM entities in cache/auth context with serialization-safe flat types (Date fields as ISO strings, relations stripped). This prevents Redis JSON round-trip issues where Date objects deserialize as strings. - Add FlatWorkspaceEntity, FlatAuthContextUser, FlatUserWorkspaceEntity, FlatApiKeyEntity types and from-*-to-flat conversion utilities - Update cache providers to convert entities before storing - Update RawAuthContext, GraphQLContext, Express Request to use flat types - Fix all consumers that called .toISOString() on already-string dates - Align test mocks with flat entity types Made-with: Cursor
...ne/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts
Show resolved
Hide resolved
There was a problem hiding this comment.
1 issue found across 42 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts:17">
P1: Avoid caching `passwordHash` in auth context user payloads; this unnecessarily propagates sensitive credential material into shared cache layers.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
...y-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts
Outdated
Show resolved
Hide resolved
Entity suffix is reserved for TypeORM entities. Rename cached flat types: - FlatWorkspaceEntity → FlatWorkspace - FlatApiKeyEntity → FlatApiKey - FlatUserWorkspaceEntity → FlatUserWorkspace Made-with: Cursor
Made-with: Cursor
Tests for UserWorkspaceService and WorkspaceAgnosticTokenService now include required date fields so fromAuthContextUserToFlat doesn't fail. Made-with: Cursor
CI typechecker (tsc) is stricter than tsgo about type cast overlap. Made-with: Cursor
packages/twenty-server/src/engine/core-modules/workspace/types/flat-workspace.type.ts
Outdated
Show resolved
Hide resolved
...wenty-server/src/engine/core-modules/api-key/services/workspace-api-key-map-cache.service.ts
Show resolved
Hide resolved
...y-server/src/engine/core-modules/auth/constants/auth-context-user-select-fields.constants.ts
Outdated
Show resolved
Hide resolved
...es/twenty-server/src/engine/core-modules/user/services/user-entity-cache-provider.service.ts
Outdated
Show resolved
Hide resolved
Flat types are a general serialization concept, not tied to caching. Move each flat type and conversion util to its entity's module: - FlatWorkspace → core-modules/workspace/types/ - FlatApiKey → core-modules/api-key/types/ - FlatUserWorkspace → core-modules/user-workspace/types/ - FlatAuthContextUser → core-modules/auth/types/ Made-with: Cursor
...ne/core-modules/logic-function/logic-function-trigger/triggers/cron/cron-trigger.cron.job.ts
Show resolved
Hide resolved
…ve passwordHash - Bind FlatWorkspace to WorkspaceEntity via Omit pattern (like FlatApplication) instead of manually listing fields - Remove passwordHash from AUTH_CONTEXT_USER_SELECT_FIELDS (query DB when needed) - Introduce FlatUser type cached directly; derive FlatAuthContextUser as Pick<FlatUser> - Replace fromAuthContextUserToFlat with fromUserEntityToFlat everywhere Made-with: Cursor
packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts
Outdated
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts
Show resolved
Hide resolved
...es/twenty-server/src/engine/core-modules/user/services/user-entity-cache-provider.service.ts
Outdated
Show resolved
Hide resolved
...src/engine/core-modules/workspace/constants/workspace-entity-relation-properties.constant.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
3 issues found across 17 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-server/src/engine/core-modules/auth/token/services/workspace-agnostic-token.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/auth/token/services/workspace-agnostic-token.service.ts:92">
P1: Using `fromUserEntityToFlat` here injects `passwordHash` into auth context user data. Return an auth-scoped user shape that excludes sensitive fields.</violation>
</file>
<file name="packages/twenty-server/src/engine/core-modules/user/services/user-entity-cache-provider.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/user/services/user-entity-cache-provider.service.ts:31">
P1: This change caches `passwordHash` in the `authContextUser` cache payload, exposing sensitive credential data in shared cache storage.</violation>
</file>
<file name="packages/twenty-server/src/engine/core-entity-cache/types/core-entity-cache-key.type.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-entity-cache/types/core-entity-cache-key.type.ts:7">
P2: `authContextUser` is typed as full `FlatUser`, which exposes sensitive fields (e.g. `passwordHash`) through the auth cache contract. Keep this key typed to the restricted auth-context user shape.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
...wenty-server/src/engine/core-modules/auth/token/services/workspace-agnostic-token.service.ts
Show resolved
Hide resolved
...es/twenty-server/src/engine/core-modules/user/services/user-entity-cache-provider.service.ts
Show resolved
Hide resolved
packages/twenty-server/src/engine/core-entity-cache/types/core-entity-cache-key.type.ts
Outdated
Show resolved
Hide resolved
Write paths (sign-up, onboarding, workspace member creation) now use fresh UserEntity from the database directly instead of converting to flat entities. Flat conversion only remains at caching boundaries. - ExistingUserOrPartialUserWithPicture.existingUser reverted to UserEntity - activateOnboardingForUser accepts Pick<UserEntity, ...> instead of AuthContextUser - createWorkspaceMember accepts Pick<UserEntity, ...> instead of AuthContextUser - setLoginTokenToAvailableWorkspacesWhenAuthProviderMatch accepts Pick<UserEntity, 'email'> - signUpInNewWorkspace fetches fresh UserEntity from DB before write operations Made-with: Cursor
packages/twenty-server/src/engine/core-modules/auth/strategies/jwt.auth.strategy.ts
Show resolved
Hide resolved
…ty types Drop ScalarFlatEntity for core entities. All flat types now use the same pattern: explicit NON_COLUMN_PROPERTIES constant + Omit + date cast. - Rename RELATION_PROPERTIES to NON_COLUMN_PROPERTIES (accurate: includes virtual fields, methods, DTOs — not just TypeORM relations) - FlatApiKey: explicit exclusion replacing ScalarFlatEntity<ApiKeyEntity> - FlatUserWorkspace: explicit exclusion replacing ScalarFlatEntity<UserWorkspaceEntity> (ScalarFlatEntity missed virtual fields like permissionFlags, objectPermissions) Made-with: Cursor
…cache The exclusion lists define what should NOT be cached, not just what's not a column. Rename to NON_CACHED_PROPERTIES to reflect actual intent. Add passwordHash to USER_ENTITY_NON_CACHED_PROPERTIES — sensitive data should never be stored in Redis cache. Made-with: Cursor
Made-with: Cursor
- Add expiresAt validation in validateAPIKey (was only checking revokedAt, allowing expired keys to authenticate) - Make cache invalidation fire-and-forget in workspace service to prevent invalidation failures from aborting workspace activation/updates Made-with: Cursor
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts:295">
P1: Do not fire-and-forget workspace cache invalidation after writes; await it to preserve read-after-write consistency and avoid silently serving stale workspace data.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts
Outdated
Show resolved
Hide resolved
The hardcoded '2025-12-31T23:59:59.000Z' expiry date is now in the past, causing all integration tests using API key auth to fail with "This API Key is expired" after the expiresAt validation was added. Made-with: Cursor
…stency Reverts fire-and-forget pattern for CoreEntityCacheService.invalidate calls in workspace.service.ts. Awaiting ensures subsequent reads see fresh data instead of stale cached values. Made-with: Cursor
Since we now cache the full FlatUser (not just FlatAuthContextUser), the cache key should reflect the actual entity being cached. Made-with: Cursor
|
|
||
| if (user.userWorkspaces.length === 1) { | ||
| await this.userRepository.softDelete(userId); | ||
| await this.coreEntityCacheService.invalidate('user', userId); | ||
| } | ||
|
|
||
| return userWorkspace; |
There was a problem hiding this comment.
Bug: Removing a user via UserService.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace fails to invalidate the userWorkspaceEntity cache, allowing the removed user to retain access via the stale cache entry.
Severity: HIGH
Suggested Fix
After the call to this.userWorkspaceService.deleteUserWorkspace in user.service.ts, add a cache invalidation call similar to the one in workspace.service.ts: await this.coreEntityCacheService.invalidate('userWorkspaceEntity', userWorkspaceId). This will ensure the cache is cleared immediately after the user-workspace link is soft-deleted.
Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.
Location:
packages/twenty-server/src/engine/core-modules/user/services/user.service.ts#L201-L207
Potential issue: When a user is removed from a workspace via
`UserService.removeUserFromWorkspaceAndPotentiallyDeleteWorkspace`, the associated
`userWorkspaceEntity` cache entry is not invalidated. The authentication logic in
`JwtAuthStrategy.validateAccessToken` retrieves this stale, soft-deleted entry from the
cache but does not check the `deletedAt` field. This allows the removed user to maintain
access to the workspace for up to 30 minutes, which is the local cache's time-to-live.
The correct behavior, which includes cache invalidation, is already implemented in
`WorkspaceService.handleRemoveWorkspaceMember`, but it is missing from the `UserService`
code path.
Made-with: Cursor
When WorkspaceMember locale is updated, the pre-query hook also updates UserWorkspaceEntity.locale in the DB but was not invalidating the userWorkspaceEntity cache. Subsequent requests would serve the stale cached locale, causing i18n metadata queries to return labels in the wrong language. Made-with: Cursor
Made-with: Cursor
|
Hey @charlesBochet! After you've done the QA of your Pull Request, you can mark it as done here. Thank you! |
Summary
Introduces multi-layer caching for the 5 most frequent database queries identified in production (Sentry data), targeting the JWT authentication hot path and cron job logic.
Problem
Our database is under heavy load from uncached queries on the auth hot path:
WorkspaceEntitylookups: 638 queries/minApiKeyEntitylookups: 491 queries/minUserEntitylookups: 147 queries/minUserWorkspaceEntitylookups: 143 queries/minLogicFunctionEntitylookups: 1800 queries/min (cron job)Solution
1. New
CoreEntityCacheServicefor non-workspace-scoped entities (Workspace, User, UserWorkspace):WorkspaceCacheServicearchitecture (in-process Map + Redis with hash validation)@CoreEntityCachedecoratorWorkspaceEntityCacheProviderService,UserEntityCacheProviderService,UserWorkspaceEntityCacheProviderService2. New
apiKeyMapWorkspaceCache for workspace-scoped API key lookups:WorkspaceApiKeyMapCacheServiceloads all API keys for a workspace into a map by IDWorkspaceCacheServiceinfrastructure3.
CronTriggerCronJobrefactored to use existingflatLogicFunctionMapsworkspace cache:LogicFunctionEntityrepository queries (~1800/min)4.
JwtAuthStrategyrefactored to use caches for all entity lookups:CoreEntityCacheServiceWorkspaceCacheService(apiKeyMap)5. Cache invalidation wired into mutation paths:
WorkspaceService→ invalidatesworkspaceEntityon save/update/deleteApiKeyService→ invalidatesapiKeyMapon create/update/revokeArchitecture
Expected Impact
Not included (ongoing separately)