Preferences
Preferences stores small per-user settings, such as colour mode, table density or a notification digest. Each setting
follows the user across devices. With OAuth Server, other apps that sign in through yours can receive it too, when you
allow it. Install it when users
should be able to choose how your app behaves for them. For settings that belong to the whole app, use module options
or app.config.ts instead.
The package is @frigear-nu/preferences. Your app and any module declare each preference once, with a zod schema.
That schema validates the API and generates the TypeScript types.
What you get
- Composables and helpers (on by default):
useUserPreferences()reads and sets preferences in the app, andgetUserPreferencereads one on the server. - Server routes (on by default):
PATCH /api/preferencesfor the signed-in user. - Types (on by default):
UserPreferencesis generated from the registered files, so a misspelled key or a wrong value does not compile. - OAuth claims (opt-in per preference): with OAuth Server, a preference can be passed to OAuth applications as a claim. A preference is private unless its definition says so.
@frigear-nu/module-kit, @frigear-nu/auth and
@frigear-nu/users.You add:@nuxthub/core.Optional:@frigear-nu/oauth-server, to pass preferences on as claims.Quick setup
Install Preferences
pnpm add @frigear-nu/preferences @nuxthub/core
Add it to nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxthub/core', '@frigear-nu/users', '@frigear-nu/preferences'],
})
Generate the migration
users table fails: the database lacks the
preferences column.The module adds a preferences column to users. Generate a migration and commit it:
pnpm db:generate
Module authors do the same in every test fixture that installs it. See Database extensions.
Declare your preferences
Declare them in shared/preferences.ts, which the module picks up automatically:
import { z } from 'zod'
import { definePreferences } from '@frigear-nu/preferences/define'
export default definePreferences({
// Plain schema: stored and readable by this app only.
compactTables: z.boolean(),
// Also forwarded to OAuth applications as the `color_mode` claim.
colorMode: { schema: z.enum(['light', 'dark', 'system']), claim: 'color_mode' },
// Defaults come from the schema.
pageSize: z.number().int().min(10).max(100).default(25),
})
await useUserPreferences().set('colorMode', 'dark') succeeds, and
get('colorMode') returns 'dark'. A value outside the schema is rejected with 400.Options
Options go under the preferences key.
| Option | Type | Default | Purpose |
|---|---|---|---|
definitions | string | string[] | shared/preferences.ts when it exists | Files that default-export definePreferences({...}). |
definitions. Paths resolve from the project root. Listing files turns off the shared/preferences.ts default.
Server
getUserPreference and getPreferenceRegistry() are auto-imported.
const user = await requireUser(event)
getUserPreference(user, 'colorMode') // typed; undefined when unset or no longer valid
Preferences are registered, not free-form. Unknown keys and invalid values are rejected. A stored value that no longer satisfies its schema reads as unset.
A module registers its own definition file from setup(), and its keys join UserPreferences the same way. The same
key in two files fails at startup.
import { addUserPreferences } from '@frigear-nu/preferences/utils'
addUserPreferences(nuxt, { path: resolver.resolve('./runtime/preferences') }) // absolute path
A definition is a zod schema, or an object with a schema and how it is exposed:
type PreferenceDefinition<S extends z.ZodType = z.ZodType>
= S | {
schema: S
/** Forward the value to OAuth applications under this claim name (ID token and `userinfo`). */
claim?: string
/** Scope an application must have been granted to receive the claim. Defaults to `profile`. */
scope?: string
}
OAuth claims
With OAuth Server, a preference that declares a claim is added to the ID token and
to userinfo. Only applications granted the definition's scope receive it.
- Access tokens stay bare.
- A preference that is not set adds no claim.
- An existing claim such as
nameis never replaced. - OAuth Server knows nothing about preferences. This module registers an
oauth:claimslistener.
App
Read and set the signed-in user's preferences:
const { preferences, get, set, pending } = useUserPreferences()
preferences.value.colorMode // 'light' | 'dark' | 'system' | undefined
get('colorMode') // the same, for one key
await set('colorMode', 'dark') // validated by the server; `null` clears it, then the session is refreshed
useUserPreferences is auto-imported. It is also exported from @frigear-nu/preferences/composables.
Hooks
The module has one build-time hook for module authors, and listens to one hook of OAuth Server.
| Hook | When | Payload |
|---|---|---|
preferences:definitions (Nuxt) | Build time, when the definition files are collected | { path: string }[]. Push to register; addUserPreferences does this. |
When OAuth Server is installed, the module listens to its Nitro hook
oauth:claims.
Routes
Routes the module registers, and who may call them. The path is preferencesApi.root from
@frigear-nu/preferences/api. @frigear-nu/preferences/utils exports it too.
| Method | Path | Auth | Status | Purpose |
|---|---|---|---|---|
PATCH | /api/preferences | session only | 200 | A partial update, for example { "colorMode": "dark" }. null clears a key. |
- The body is validated against a zod schema built from the registered definitions.
- The answer is the stored preferences (
PreferencesDto), not the user row. - The route refreshes the session.
| Status | When |
|---|---|
400 | The body is not an object, or has an unknown key or an invalid value. Nothing is stored. |
401 | No session. |
403 | An API token or an access token. |
404 not_found | The user no longer exists. |
Database
Tables the module adds. Generate and commit a migration after installing.
The module adds one nullable JSON column to users, through a
database extension: preferences (Record<string, unknown>).
- Every preference lives in this one column, so adding a preference never needs a migration. Only installing the module does.
- SQLite and PostgreSQL can filter and index JSON keys when a preference must be queried.
Next steps
- Users – the user record your preferences are stored on.
- OAuth Server hooks – add more claims to the ID token in
oauth:claims.