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.