Skip to content
Tikab's Toolkit

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 getConfig freely; 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).