Runtime config
The constance counterpart: settings an admin changes at runtime, without a
deploy — feature toggles, copy, limits. Values are plain JSON in the app_config
table, read through a small cached API.
The API
Live from the source:
export async function getConfig<T>(key: string, fallback: T): Promise<T> {
const hit = cache.get(key);
if (hit && hit.expires > Date.now()) return hit.value as T;
const row = await db.query.appConfig.findFirst({ where: eq(appConfig.key, key) });
const value = row ? (row.value as T) : fallback;
cache.set(key, { value, expires: Date.now() + TTL_MS });
return value;
}
/** Upsert a key and drop it from the local cache. */
export async function setConfig(key: string, value: unknown): Promise<void> {
await db
.insert(appConfig)
.values({ key, value, updatedAt: new Date() })
.onConflictDoUpdate({ target: appConfig.key, set: { value, updatedAt: new Date() } });
cache.delete(key);
}Semantics worth knowing
- The fallback is mandatory — a missing key never branches your app into an
undefined state.
getConfig("max_upload_mb", 25)always yields a number. - The cache is per-process with a 30s TTL. Hot paths can call
getConfigfreely; an admin's change lands everywhere within the TTL without any cross-instance invalidation machinery. (A shared cache can replace this when instances multiply.) listConfig()is uncached — admin surfaces want the truth.- Valid JSON is stored as JSON (numbers, booleans, objects); anything else is stored as a plain string. The admin UI applies that rule on save.
The admin surface
The example exposes the table at /admin/config — a changelist where editing a row
prefills the form. Every change writes an audit row
(admin.config.set).