Appearance
Config Store
Concept layer — frozen. The Config Store generic. Nothing here is written by a normal playbook run; a project's own feature analysis is the live document and takes every edit. This layer names no project and links to none — the dependency runs one way, from an application to its concept.
Business Context
This feature provides the ability to dynamically configure global application settings without modifying the source code. It is not intended to replace environment properties, as the configuration store is not secure — its values are accessible to administrators. It should therefore only be used for non-sensitive data such as administrator contact information, branding codes and similar settings.
Business-Level Definition
The system supports a generic configuration mechanism using a key–value mapping stored in a database table. The configs are environment-scoped and tied to a specific system instance.
Requirements Definition
- Introduce a mechanism for storing configurations at the database level, allowing admin roles to modify them at runtime. Unlike environment properties, which require system administrator access and a restart to change, these configurations can be adjusted dynamically without downtime.
- Avoid the need to redeploy or patch code for configuration changes.
- Each configuration entry can have its type explicitly defined (for example string, email), allowing typed access and validation logic in consuming systems.
Technical Context
User Stories / Use Cases
- As an admin, I want to update a configuration property without developer assistance.
- As a backend system component, I need to retrieve specific instance-wide settings during runtime (for example the maximum file upload size).
- As a frontend component, I want to load branding data (name, logo, colors) dynamically based on configuration.
Functional Requirements
- Admins can create, read, update and soft-delete configuration entries.
- Supported types: see Enum — ConfigValueType.
- The system validates format and type before saving values.
- The system validates the format and type of these values upon retrieval, helping to prevent runtime errors caused by malformed configurations.
- Entries are environment-bound and immutable once deleted.
- The system offers both an API for administrators and an internal backend service designed for internal use.
Non-Functional Requirements
- Performance: configuration values must be fast to retrieve (under 100 ms). A cache is one way to get there and, for a store of a few dozen rows read from the same database as everything else, not the only one.
- Transactional operations: where a cache exists, it must be refreshed after the table data is updated.
Domain Model & Entity
configStore— the configuration entity.- Enum — ConfigValueType — the supported value types.
API Analysis (API-A)
See child page: Generic Config Store API.
Logging & Monitoring
.info
- for create, update and delete actions.
.error
- on validation errors.
Caching & Performance
- Config values may be cached in memory on application start and refreshed periodically. A store read once per request from a table of a few dozen rows is within the concept; a project that caches nothing has taken the simpler consistency guarantee, not deviated.
- Where a cache exists: TTL = 60 seconds (configurable), and both mechanisms below apply.
Both mechanisms apply, and each covers what the other cannot. Refreshing on write keeps the instance that made the change correct immediately. The TTL bounds how long any other instance can serve a stale value — because a per-process in-memory cache receives no invalidation from a write that happened elsewhere. With more than one replica the TTL is not a fallback; it is the only thing that propagates a change at all, and it sets the real consistency guarantee: up to one TTL behind, everywhere except where the write landed.
A project that moves this cache to a shared store should say so, because the trade-off changes: invalidation then reaches every reader, and the TTL becomes a genuine backstop.
Legal Context
The mechanism itself — a typed key–value table — carries no licensing or regulatory obligation. What it holds may. These are the questions to answer before shipping, because the answers differ by jurisdiction, industry and contract.
- Does the store hold personal data? A "settings table" sounds like it could not, but the settings a business wants to edit without a release include contact names and addresses. If any entry does, decide how a rectification or erasure request reaches a store no data-subject index knows about, and whether the entry is even in scope — a published business contact is a different case from a private one, and a setting republished on an unauthenticated page is disclosed to the world rather than to administrators.
- Do any values point at documents the project is legally obliged to publish? Policy and terms links are the usual example. The obligation belongs to the surface that displays them, but the store decides how fast a wrong link can be corrected and whether "which terms were in force on this date" can be answered at all — a store that overwrites in place cannot answer it.
- Are any of these settings a control someone expects to be change-managed? A value that turns a feature on, widens a limit or relaxes a check changes system behaviour outside the release process, with none of its review. Whether a contractual security schedule or an assurance regime counts that as a change is a question for whoever owns the commitment; if it does, the store needs an approval or attribution story a purely technical design would not give it.
- Is anything in here required to be held under specific protections? Encryption at rest, restricted access and rotation attach to secrets, and this mechanism provides none of them. If the answer is "no secrets are stored here", that is a commitment someone has to keep.
- Is there a retention obligation on the history of a setting? Whether a configuration change is a record for audit or evidential purposes is a legal judgement; what follows technically is not, because a table with no history has already decided the answer.
Cybersecurity Considerations
The decision this concept refuses to make: does the store hold secrets? It determines everything else about who may read the table, and must be taken explicitly rather than discovered later from what someone stored.
- A deliberately non-secret store. Readable by any account holding the read permission, and typically republished in part to unauthenticated clients. No encryption, no per-entry read authorisation, no rotation — and none needed. The cost is a second mechanism for anything sensitive, and a boundary held by convention: nothing in a typed key–value table refuses an access key stored as a string, and the type check will not notice.
- A secret-bearing store. One mechanism instead of two, at the price of encryption at rest, per-entry read authorisation, redaction in logs and in any administration surface, and a rotation story. The store stops being cheap, and every path that reads it becomes a path that has to be trusted.
Either is defensible. The failure is the undeclared middle: a store documented as non-secret with nothing preventing a secret from entering it. Back the boundary with something mechanical — a separate store for sensitive values, a per-entry sensitivity flag, a naming convention with a check behind it — rather than a sentence.
Blast radius is the other half of the threat model. A write here has some of the reach of a release with none of a release's controls: no review, no staged rollout, no revert distinguishable from another edit. The recurring worst cases are a switch that enables a feature for everyone at once, an address an integration will call — changing it redirects data to a destination of the writer's choosing — a limit gating what may be uploaded, and a diagnostic toggle exposing internals.
Two decisions follow, and both are commonly inherited rather than taken:
- Read and write authorisation are separate, and write is the privileged one. Splitting further — per entry, or per class of entry — is worth considering precisely for the high-blast values; the alternative is to keep those values out of the store entirely.
- An approval step is a real option and a real trade. The store's whole value is that a change takes effect immediately; requiring a second person trades that away. A project may want it for one class of setting and not another, but it should choose rather than inherit "instant, unreviewed" by default.
Three properties hold whatever is decided. Validation by type is not validation against a policy — a well-formed link is still a link to anywhere, and only an allowlist addresses the redirection case. Values are not sanitized for their destination, so a setting rendered into a page or interpolated into a query is an injection question owned by the consuming surface. And full-row write logging, a common persistence-layer default, puts the value in the logs on every write, so anything mistakenly stored here outlives its deletion — decide whether that is acceptable for this store before deciding it is non-secret, because the two decisions are the same one.
The scope. A deployment-wide store has no scope boundary to breach, and equally no way to give one customer a different value. A project that adds a scope dimension inherits the whole of the Multi-Organizations / Tenant Scoping concept here, including the rule that the scope belongs in the cache key.
Whatever the store's scope, the write permission must be evaluated at that scope. A deployment-wide store written under a permission that any single tenant's administrator can hold is a cross-tenant write path: one tenant's administrator rewrites the sign-in page, the policy links or the upload limit for every tenant on the instance. The platform's "permission held in any organization" fallback — convenient for requests that carry no scope, and often inherited from the authorisation layer rather than chosen — is exactly the mechanism that opens it. A deployment-wide store's write routes are elevated routes and are authorised as such; a scoped store's write routes check the permission in the scope being written.
Risk Assessment
Business Risks
The exposure is asymmetric between wrong and unavailable.
- Wrong. An incorrect value reaches users at exactly the speed a correct one does, and the correction is equally fast. A favourable trade for a mistyped contact address and a poor one for a setting whose blast radius is large — the same trade in both cases, which is why classes of setting deserve different treatment.
- Unavailable. If the store is read during start-up, its availability is the availability of the application starting; as a table on the same database as everything else that adds no independent exposure. A project that moves configuration into a separate service must say what happens when it is down: fail closed, or fall back to values compiled into the application. Falling back is kinder and quietly reintroduces the release-coupling the store exists to remove.
- Not changeable in practice. Runtime editability exists only for settings that have an editing path; one reachable only by seeding is changed by a developer, a migration and a release. The cost is turnaround, not an outage, and the honest record is to name which settings have a surface.
- Nobody owns the key space. An unregistered set of codes accumulates entries no consumer reads, and lets a consumer name a code nobody stored. A registry costs a place to keep it in step; its absence costs a class of silent typo.
- The store attracts values that have nowhere else to live. A prompt, a template or a feature switch stored "for now" — usually under a code that ignores the naming convention and behind a bespoke write path with its own permissions rather than the generic API — becomes a second product surface. The registry should say which classes of value are welcome, and a migration out is the normal end of the "for now".
Technical Risks
- Staleness, where a cache exists. The mechanics are under Caching & Performance; the risk is that the real consistency guarantee is the TTL rather than the refresh-on-write, so a project reading only the refresh half believes changes propagate immediately when they do not.
- Multi-value writes that are not one transaction. A form writing several settings in a loop can fail part-way, leaving some applied and the rest not, with no rollback and no marker. Grouping them is cheap; noticing that they were not grouped is not.
- Soft delete plus uniqueness across deleted rows. Reserving a retired code stops it being quietly reused for a different meaning — the reason to do it — but recreation then fails with a duplicate error reading like a conflict rather than a tombstone, and recovery is a data migration. The related trap: an update writing onto a soft-deleted row without clearing the deletion marker leaves the value stored and invisible.
- Validation applies only to the path that has it. Values arriving by seed or migration bypass the API's type check. Re-validating on read closes the gap at the cost of a check on every read and a decision about a row that fails one; trusting the write path is cheaper and serves a bad row as-is. If the persistence layer re-validates the whole row on write — including on soft delete — a bad seed row is not merely served as-is; it is also unfixable through the API, because the correction and the retirement are refused for the same reason the row is wrong, and the escape is a migration. Either way a value can be well-formed and still wrong, and nothing detects that.
- How an unresolved code is answered shapes what can go wrong. Returning null for every code that matched nothing means callers never have to distinguish "absent" from "empty" — and never learn that a code was misspelled or deleted. Returning an error surfaces the mistake and makes a missing optional setting fatal. Choose, and pair the lenient answer with the measurement below.
- Concurrent updates are last-writer-wins unless an optimistic check is added. Uniqueness protects against a double create; nothing protects against two administrators editing the same setting a second apart.
Auditing, Reporting & Measurement
A configuration table answers what is this value now. Everything else a reader will eventually want to ask has to be designed in, and usually is not.
- Attribution is the first thing to get right and the easiest to lose. Created-by and updated-by columns are commonly filled with a constant marker naming the path rather than the acting person, recording how a row was changed and not by whom — while the actor was available on the request and simply not threaded through to the write. For a publicly visible value, "who changed this" is the whole question.
- Overwrite in place means there is no history. The previous value is gone at the moment of the update, so what did this setting say last month is unanswerable from the table. If Legal Context turned up an obligation to know what was published when, the previous value must be preserved deliberately — as a change trail alongside the row, or by routing configuration changes into the Audit Log concept.
- Whether config changes belong in the audit trail is a genuine choice. For: a configuration change is a behaviour change with much of the reach of a release, and the release has its own record while this does not. Against: volume and noise, and a trail that fills with branding tweaks. Volume is not a reason to leave a class of entry out, though — the Audit Log concept handles noise at read time, marking events feed-visible or not while the quiet ones stay in the trail and stay readable to the investigation surface. So record every configuration change and mark the low-consequence ones out of the feed. Auditing only the high-blast-radius settings buys the same quiet feed and loses the entry in the one case that matters: a value nobody was watching moved.
- Settings that arrive by seed have their change record outside the product. The deployment changeset is a real and reviewable trail, answerable only by reading a changelog and appearing in no history a user or auditor is shown.
- Nothing counts reads, and nothing alerts when a code a consumer expects resolves to nothing. A missing setting presents as a blank space on a screen, not as an error, so the first report is usually a person noticing. The cheapest measurement that would change this is a count of code lookups that matched no row — it catches a typo in a reader, a retired setting still being asked for, and an incomplete deployment, all with one number.
Known instances
None recorded in this copy. Add this project's instance here as it adopts the concept — one line naming the feature that implements it. This is the only place a concept may name a project, and the template ships it empty so a fork inherits no other project's entries.