Notification Preferences
Notification Preferences
Per-user granular control over which notification types they receive. Stored in the shared settings table (no encryption , this is a plain boolean toggle, not a secret), surfaced in Settings → Notifications.
Architecture
- Uses
getSetting/setSettingfrom@/lib/db/settings-helpers.serverover the sharedsettingspgTable (composite(user_id, key)primary key) , seearchitecture/postgres-port.mdfor the DB layer, and note the base template's per-user encrypted settings API (getEncryptedSetting/setEncryptedSetting) was dropped during the Postgres port , it had zero callers. This feature never needed encryption to begin with (a boolean toggle, not a secret). - Default (no row): marketing emails opt-in
- Dedicated query key in
query-keys.tsfor cache management - Server fn:
getNotificationPreferencesFn(GET),updateNotificationPreferencesFn(POST)
export const updateNotificationPreferencesFn = createServerFn({
method: 'POST'
})
.validator(z.object({ marketingEmails: z.boolean() }))
.handler(async ({ data }) => {
const user = await requireUser();
if (!user) return err(ERROR_CODES.UNAUTHORIZED, 'Not authenticated');
if (notificationPreferencesLimiter.check(user.id))
return err(ERROR_CODES.RATE_LIMITED, 'Too many requests');
return withUserTransaction(user.id, async (tx, userId) => {
await setSetting(
tx,
userId,
MARKETING_EMAILS_KEY,
data.marketingEmails ? '1' : '0'
);
return ok(undefined);
});
});
Why the settings table, not the notifications table
This feature and the in-app notification bell (docs/warpkit/features/notifications.md) look similar but store fundamentally different things: the notifications table holds a growing list of discrete, timestamped bell items (append-only, one row per event, read/unread state per row). Notification preferences are a single small set of boolean/string toggles per user that get overwritten in place, never appended to or listed. There's no query pattern here that benefits from a dedicated table (no "list my preference changes," no per-row read state) , a settings key-value row is the right shape, and reusing the same settings helper as other per-user config (e.g. sidebar preferences) avoids introducing a second storage mechanism for what is structurally identical data.
Comparison to Notifications
| Concern | Notifications | Notification Preferences |
|---|---|---|
| What it stores | In-app bell items | User preference toggles |
| Storage | notifications table (shared Postgres) | settings table (shared Postgres) |
| Mutation side | Created by notify() helper | Updated by user in settings |
| Query layer | notificationsQueryKey | notificationPreferencesQueryKey |
Default behavior
With no settings row present (new user, or a key never written), getNotificationPreferencesFn returns marketingEmails: true — new users are opted in to marketing email by default, matching the signup flow's assumption that a user who just created an account wants product updates until they explicitly opt out.
Tests
Covered in src/features/notification-preferences/server/notification-preferences-crud.test.ts.