Per-User Settings

Per-User Settings

Typed get/set for per-user app settings, stored in the shared settings table (composite (user_id, key) primary key).

Schema

Already in src/lib/db/schema.ts, no migration needed to use it, only to add a new key's shape if you're storing structured data instead of a plain string.

Reading and writing settings

Use helpers from src/lib/db/settings-helpers.server.ts:

import { getSetting, setSetting } from '@/lib/db/settings-helpers.server';

// Read (returns null if not set)
const apiKey = await getSetting(db, userId, 'openai_api_key');

// Write (upsert)
await setSetting(db, userId, 'openai_api_key', 'sk-...');

No built-in encryption. The base template's encrypted variants (getEncryptedSetting/setEncryptedSetting, AES-256-GCM, SETTINGS_ENCRYPTION_KEY) were dropped during the Postgres port, they had zero callers in the source at the time, so they weren't carried forward rather than porting unused surface area. If you need to store a real secret this way, re-add an encrypt/decrypt wrapper around setSetting/getSetting rather than assuming one still exists, don't import getEncryptedSetting/setEncryptedSetting, they no longer exist.

In a mutation

return withUserTransaction(user.id, async (tx, userId) => {
  await setSetting(tx, userId, 'openai_api_key', data.apiKey);
  await logUserEvent(tx, userId, 'settings.updated', { key: 'openai_api_key' });
  return ok({ success: true });
});

Gate ordering note

Reading a setting to gate a mutation (e.g. "does API key exist?") is a read-only gate, do it before withUserTransaction. Writing a setting is a mutation, do it inside the transaction, using the tx handle. See Gate ordering.