PrimeBrickPrimeBrick
  • Docs
  • Contact
  • MIT License
  • Documentation
  • MCP Server
  • API Catalog
  • Services
  • Libraries
PrimeBrickPrimeBrick

© 2026 PrimeBrick. MIT License. v3.8.0

github
DAL Library
SDK Library
    OverviewGetting StartedAuthenticationExt-JSONRedis cache layerConfig tables & ConfigLoaderNATS™ ClientService RegistrationHTTP ServerSSE StandardPresenceAPI Reference
powered by Zudoku
SDK Library

Redis cache layer

Primebrick provides an optional Redis cache layer for hot single-row reads. It is a feature, not a requirement — the system is fully valid without it. If Redis is not configured or unreachable, the system runs exactly as today (DB-only), with warn logs (never error).

What gets cached

  • Only single-row finders: findById, findByUUID, find.
  • Only entities marked @Cached(). Other entities pass through untouched.
  • findAll and findByPage are NOT cached — high-cardinality keys, memory bomb risk on large tables, stale-on-write window dangerous for list views. Cache dropdown/autocomplete lists at the BE application level with hand-written be:dropdowns:* keys via the same CachePort.

What does NOT get cached (and why)

OperationCached?Reason
findByIdYesSingle row, stable key from result row
findByUUIDYesSingle row, input IS the cache key
findYesReturns 1 row by construction (limit: 1)
findAllNoList — high cardinality, stale risk
findByPageNoPaginated list — same as above
Writes (add, update, …)No (invalidate)Writes go DB-first, then invalidate

Enable Redis

redis_url is a new optional key in the auth_configurations table. Empty or missing = cache disabled. Set it to your Redis URL:

Code
INSERT INTO "public"."auth_configurations" ("key", "value", "description", "created_by") VALUES ('redis_url', 'redis://localhost:6379', 'Redis cache URL', 'system') ON CONFLICT ("key") DO NOTHING;

The BE reads redis_url at startup via loadAuthConfig. If Redis is unreachable, the BE logs a warn and continues without cache — no fail-fast, no crash.

Microservices do NOT set redis_url in their own config tables. They discover it from the BE via the NATS config.get protocol (see Redis for microservices below). The SQL INSERT above applies only to the BE's auth_configurations table.

Redis for microservices (mandatory SDK pattern)

Microservices must connect to Redis if they use @Cached() entities. Without a shared Redis connection, cache invalidation from the BE (or other microservices) cannot propagate, and the microservice would serve stale data from its own Redis reads.

redis_url is stored only in the BE's auth_configurations table — it is never duplicated in microservice config tables. Microservices discover it from the BE via the NATS config.get request/reply protocol (see Shared config protocol below).

The SDK provides initCacheFromSharedConfig(natsClient, logger) as a one-liner that handles the NATS request, Redis connection, version logging, and error handling. Microservices MUST use this instead of calling createRedisClient directly with a hard-coded URL.

Microservice side (one-liner)

Code
import { NatsClient, initCacheFromSharedConfig } from "@primebrick/sdk"; // After NATS connection is established: await NatsClient.getConnection(natsUrl); const { cachePort } = await initCacheFromSharedConfig(NatsClient, console); if (cachePort) { console.log("Redis cache enabled for my-service"); } else { console.log("Redis cache disabled for my-service (best-effort)"); }

If a microservice does not use @Cached() entities, Redis is optional — but calling initCacheFromSharedConfig is still recommended so the startup banner is consistent across all services.

BE side (subscribe to config.get)

The BE subscribes to config.get on NATS and responds with the shared config object. This is done in the BE's startup sequence, right after the NATS connection is established:

Code
import { subscribeSharedConfig } from "@primebrick/sdk"; import { getAuthConfig } from "./modules/auth/config.js"; await NatsClient.getConnection(); await subscribeSharedConfig(NatsClient, () => { const cfg = getAuthConfig(); return { redis_url: cfg.redis_url }; });

The getConfig function is called on each request — it reads from the BE's in-memory auth config (already loaded at startup via loadAuthConfig).

Shared config protocol (NATS config.get)

config.get is a generic NATS subject for sharing configuration from the BE to microservices. It is not Redis-specific — future shared config fields (s3_url, feature_flags, etc.) can be added to the SharedConfig interface without changing the protocol or breaking consumers.

The SharedConfig interface

Code
import type { SharedConfig } from "@primebrick/sdk"; interface SharedConfig { /** Redis cache URL. Empty/undefined = cache disabled. */ redis_url?: string; // Future: s3_url?, feature_flags?, etc. }

All fields are optional: the BE only includes what it has configured. The microservice checks individual fields rather than assuming the whole object is populated.

Request/reply flow

Timeout and best-effort behavior

The NATS request has a 5-second timeout. If the BE doesn't respond (e.g. BE is still starting up, or NATS is flaky), fetchSharedConfig returns an empty object {}. The microservice continues without Redis — all cache calls are no-ops. The system is fully valid without Redis.

Direct API (advanced)

For cases where the one-liner initCacheFromSharedConfig is too coarse, the SDK exposes the building blocks:

Code
import { fetchSharedConfig, createRedisClient, RedisCachePort, getRedisInfo } from "@primebrick/sdk"; const shared = await fetchSharedConfig(NatsClient); if (shared.redis_url) { const redis = await createRedisClient(shared.redis_url); const cachePort = new RedisCachePort(redis); const info = await getRedisInfo(redis); console.log(`[cache] Redis connected (v${info?.version ?? "unknown"})`); }

Health endpoint & version logging

Startup banner

Both the BE and microservices log a startup banner showing the Redis connection status and server version:

StateLog output
Connected[cache] Redis connected (v7.4.0)
Connected (version unknown)[cache] Redis connected (version unknown)
Not configured[cache] redis_url not set — cache disabled (best-effort, system valid without it)
Not received from BE (microservice)[cache] redis_url not received from BE — cache disabled (best-effort)
Connection failed[cache] Redis connection failed — cache disabled: <error>

The version is queried via getRedisInfo(redis) which calls the Redis INFO command and parses redis_version from the # Server section.

BE health endpoint

The BE's GET /api/v1/health now includes a redis field:

Code
{ "ok": true, "service": "primebrick-api", "version": "0.30.0", "db": { "ok": true }, "idp": { "ok": true, "type": "Casdoor", "version": "1.x.x" }, "redis": { "ok": true, "version": "7.4.0" } }

When Redis is not configured or unreachable:

Code
{ "redis": { "ok": false } }

The URL is never exposed in the health endpoint — only the connection status and server version.

FE VersionsPanel

The Frontend's VersionsPanel (sidebar sheet) shows a "Redis" row after the Identity Provider row, with:

  • An "Online" badge (green) or "Offline" badge (red)
  • A version badge (e.g. 7.4.0) or unknown

The Redis URL is not shown in the FE — it is only in the BE console log.

Mark an entity as cacheable

Import @Cached and @CacheKey from @primebrick/sdk and decorate your entity:

Code
import { Cached, CacheKey } from "@primebrick/sdk"; import { Entity, Key, Column } from "@primebrick/dal-pg"; @Entity("customers") @Cached(300_000) // 5 minutes TTL — for mutable data export class CustomerEntity { @Key() id!: bigint; uuid!: string; // CacheKeyBuilder falls back to row.uuid @Column({ pgType: "varchar" }) name!: string; }

Choosing a TTL

  • @Cached() with no argument = no TTL, immutable data only. Use this ONLY for genuinely immutable data (the cached value can never change). There is NO implicit default — omitting the TTL is a deliberate statement that the data is immutable.
  • @Cached(300_000) = 5 minutes. Recommended starting point for mutable data. The TTL bounds the staleness window if Redis is intermittently unavailable during invalidation.
  • The TTL is a correctness parameter, not just a performance one. Pick a TTL that bounds how stale a read can be in the worst case (Redis down during a write invalidation).

Choosing the cache key

CacheKeyBuilder derives the key from the result row, never from the input argument. Resolution order:

  1. The property marked @CacheKey() → dal:{table}:{row[propertyKey]}
  2. Else row.uuid (JS property convention) → dal:{table}:{row.uuid}
  3. Else the @Key() column (read via Reflect) → dal:{table}:{row[keyPropertyKey]}
  4. Else throw — add @CacheKey() to the property to use as the cache key.

Why result-row-derived keys? findById(42) and findByUUID(<uuid>) on the same row produce the SAME cache key. This avoids duplicate entries and ensures invalidation works correctly.

When to use @CacheKey() explicitly:

Code
@Entity("idp_code_map") @Cached() // No TTL — immutable mapping export class IdpCodeMapEntity { @CacheKey() idp_code!: string; // Use idp_code as the cache key uuid!: string; }

Use @CacheKey() when:

  • The entity has no uuid property.
  • You want the cache key to match the FE-facing identifier (usually uuid — but @CacheKey makes it explicit).
  • The entity has multiple unique columns and you want a predictable key.

Wire the cache into your Repository

The withCache wrapper is opt-in. Call it once at bootstrap:

Code
import { withCache, RedisCachePort, createRedisClient } from "@primebrick/sdk"; import { Repository } from "@primebrick/dal-pg"; // In your BE startup (after loadAuthConfig): if (authConfig.redis_url) { const redis = await createRedisClient(authConfig.redis_url); const cachePort = new RedisCachePort(redis); // Wrap each Repository instance once at creation: const repo = withCache(new Repository(pool), cachePort, logger); // Use `repo` as normal — cache is transparent. }

If redis_url is empty or Redis is unreachable, skip withCache — the bare Repository works exactly as before.

Failure behavior (best-effort)

The withCache wrapper NEVER lets a cache failure break a request:

FailureBehavior
Redis down on readwarn log, fall through to DB, return the row
Redis down on writeDB write succeeds first, invalidation fails with warn
Redis down on hydrateRead returns the DB row; set failure is fire-and-forget
CacheKeyBuilder throwswarn log, fall through to DB

The caller never sees a cache error. All failures are warn logs, never error. The cache is a feature, not a requirement.

Serialization

Cache values are serialized with the SDK's canonical extJsonStringify / extJsonParse (json-bigint, useNativeBigInt: true) — the same serializer used for HTTP responses and NATS™ messages. bigint PKs and Date fields round-trip correctly. No custom $bigint: hack.

See Ext-JSON for details on the serialization format.

Cache keys

Keys are namespaced as dal:{tableName}:{identifier}:

EntityKey example
CustomerEntity (@Entity("customers"))dal:customers:abc-123-uuid
IdpCodeMapEntity (@Entity("idp_code_map"), @CacheKey() idp_code)dal:idp_code_map:ACME
Entity without @Entity (no DAL)dal:MyClass:abc-123 (falls back to class name)

The table name is read from the DAL's @Entity decorator via standard JS reflection (Reflect.getMetadata). The SDK has zero dependency on the DAL — no import, no package dependency. The DAL is completely untouched by the cache layer.

Multi-instance BE (pods behind a load balancer)

Redis is the single shared cache. When pod #1 invalidates a key, pod #2's next read sees the miss in Redis and re-hydrates from PostgreSQL. There is no cross-pod stale-cache problem. No NATS™ invalidation broadcaster is needed.

An L1 in-process cache in front of Redis is deferred — it would require a NATS™ broadcaster to stay consistent across pods, and Redis latency is not measurably painful at fewer than 5 pods. If L1 becomes needed, it will be a separate plan.

Under the hood (for contributors)

  • The cache module lives entirely in @primebrick/sdk (src/cache/). The DAL is NOT involved — it has zero cache knowledge.
  • The SDK reads entity metadata (table name, key column) via Reflect.getMetadata("primebrick:tableName", ctor) — the DAL's @Entity and @Key decorators write this metadata via Reflect.defineMetadata. No package dependency between the SDK and the DAL.
  • @Cached and @CacheKey use the SDK's own WeakMap — separate from the DAL's ClassEntityMeta. Two metadata systems coexist without interacting.
  • withCache uses a structural CacheableRepository interface — TypeScript® structural typing means a DAL Repository is assignable without any import type from the DAL.
  • RedisCachePort uses node-redis (the redis npm package, v6.x) — the official Redis client, recommended by Redis org for new projects.

Next steps

  • Ext-JSON — the serialization layer used by the cache
  • Authentication — auth modes, RBAC, session context
  • API Reference — every exported symbol, including CachePort, CacheKeyBuilder, Cached, CacheKey, withCache, RedisCachePort
Last modified on July 26, 2026
Ext-JSONConfig tables & ConfigLoader
On this page
  • What gets cached
  • What does NOT get cached (and why)
  • Enable Redis
  • Redis for microservices (mandatory SDK pattern)
    • Microservice side (one-liner)
    • BE side (subscribe to config.get)
  • Shared config protocol (NATS config.get)
    • The SharedConfig interface
    • Request/reply flow
    • Timeout and best-effort behavior
    • Direct API (advanced)
  • Health endpoint & version logging
    • Startup banner
    • BE health endpoint
    • FE VersionsPanel
  • Mark an entity as cacheable
    • Choosing a TTL
    • Choosing the cache key
  • Wire the cache into your Repository
  • Failure behavior (best-effort)
  • Serialization
  • Cache keys
  • Multi-instance BE (pods behind a load balancer)
  • Under the hood (for contributors)
  • Next steps
TypeScript
TypeScript
TypeScript
TypeScript
JSON
JSON
TypeScript
TypeScript
TypeScript