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.