Config modules
Config modules
Primebrick modules (the Backend's auth module, the emailsender
microservice, and any future module) store their runtime configuration in
dictionary-style config tables. This page is the cross-cutting
architectural overview; the API-level reference lives in the
SDK guide → Config tables & ConfigLoader.
What is a config module?
A "config module" is any module that owns a config table and uses the
SDK's ConfigLoader<TResult> to read it. Concretely:
- The module has a DB table named
configin its own schema (public.configfor the BE auth module,emailsender.configfor the emailsender microservice,billing.configfor a future billing module). - The table has the standard column set defined by
ConfigEntityBase(key,value,type,enum_values,label_key,description_key, plus audit + soft-delete columns). - The module declares a typed interface (
TResult, e.g.AuthConfig) whose field names match the DB keys. - The module wires a
ConfigLoader<TResult>at startup, callsload()once, and usesget/requireon the hot path. - The module exposes the table to the Frontend via the standard entity CRUD
path
/api/v1/entities/config_entries/.... - The Frontend renders the table with the reusable
ConfigTablecomponent — no per-module form code.
Why a shared standard
Before the standard, the BE auth module used a bespoke
auth_configurations table with a parallel AuthConfigCache, and the
emailsender microservice used a config table with manual parseInt at
every call site. Two different patterns for the same problem meant:
- No reusable admin UI — each module's settings page was hand-coded.
- No shared type coercion — each consumer re-implemented
parseInt,=== "true", etc. - No i18n labels — the FE had to hard-code setting titles.
- Drift — the two implementations diverged over time.
The standard collapses both into one pattern: any new module gets caching,
typed access, i18n labels, and an admin UI for free by extending
ConfigEntityBase and using ConfigLoader<ItsConfig>.
The three layers
Layer 1 — DAL: ConfigEntityBase
@primebrick/dal-pg provides a base entity class with the standard column
set. A module's entity extends it and only declares the table + schema:
Code
Layer 2 — SDK: IConfigEntity, ConfigRepositoryPort, ConfigLoader<TResult>
The SDK defines the row shape (IConfigEntity), the DB-agnostic read port
(ConfigRepositoryPort), and the generic typed loader
(ConfigLoader<TResult>). The loader reads all rows once, coerces each
value by its type column, and builds the consumer's TResult. See
SDK guide → Config tables & ConfigLoader for
the full API.
Layer 3 — Consumer: TResult + adapter + validate
The consumer module owns:
- The
TResultinterface (e.g.AuthConfig) with field names matching the DB keys. - A
ConfigRepositoryPortadapter that uses its DAL to read rows. - A
validate()function for mandatory-field checks (the SDK does not enforce these — they are module-specific).
Frontend: reusable ConfigTable
The FE has one ConfigTable component that renders any config table as a
two-column layout (label + description on the left, widget on the right).
The widget is chosen from the row's type column. A new module's settings
page is just a route that fetches the list endpoint and renders
<ConfigTable />.
Type vocabulary
The type column is the single source of truth for both SDK coercion and
FE widget selection. The vocabulary is fixed and shared:
type | SDK coercion | FE widget |
|---|---|---|
string | as-is | Input |
text | as-is | Textarea |
boolean | === "true" | Switch |
integer | parseInt | Input type="number" |
number | parseFloat | Input type="number" |
enum | validated against enum_values | ComboSelect |
url | as-is | Input type="url" |
secret | as-is | Password (masked, write-only) |
json | JSON.parse | Textarea |
See the SDK guide for the full rules.
Caching
There are two complementary caches:
ConfigLoaderin-memoryMap— the typed dictionary, loaded once at startup. Hot path (get/require) never touches the DB. Invalidated byinvalidate()+load()after a config update.- Redis
withCachelayer (optional) — caches single-row DAL reads (findByUUID,findById) for entities marked@Cached(). Invalidated automatically on DAL writes.
A config update must invalidate both: the DAL write triggers Redis
invalidation (if @Cached() is set), and the route handler explicitly
calls configLoader.invalidate() + configLoader.load() to refresh the
typed dictionary.
Reference implementations
| Module | Table | TResult | Schema |
|---|---|---|---|
| BE auth | config | AuthConfig | public |
| emailsender | config | EmailSenderConfig | emailsender |
New modules should mirror one of these. The BE auth module is the canonical reference for a monolithic-module config table; the emailsender microservice is the canonical reference for a microservice config table.
Adding a new config module
- Create a schema-qualified
configtable (extendConfigEntityBase). - Seed the rows with
key,value,type,enum_values(ifenum),label_key,description_key. - Declare the module's
TResultinterface with field names matching the DB keys. - Implement
ConfigRepositoryPortwith the module's DAL. - Wire
ConfigLoader<TResult>at startup, callload(), thenvalidate(). - Expose
/api/v1/entities/config_entries/...(meta, list, get, update) with secret masking on reads. - Add a Frontend route that renders
<ConfigTable />against the list endpoint. - Add the
label_key/description_keytranslations to the FE i18n files.
No SDK, DAL, or FE component changes are needed for a new module — the standard handles it.
Next steps
- SDK → Config tables & ConfigLoader — full API
reference for
IConfigEntity,ConfigRepositoryPort,ConfigLoader<TResult>. - SDK → Redis cache layer — the complementary single-row cache.
- Microservices → Architecture — how microservice config tables are proxied through the BE.
- Architecture — the overall layered architecture.