@theshoregroup/settings-core
v0.3.0
Published
Typed, identity-scoped key-value preferences system. Pure TypeScript — no HTTP, no ORM, no auth-library coupling.
Readme
@theshoregroup/settings-core
A simple way to store typed settings against a user or organization identity, use with @theshoregroup/settings-better-auth-adapter
Quickstart
Install
pnpm add @theshoregroup/settings-core zod@v4Configuring
Persistence
To persist settings, use a store. This library includes an in-memory store suitable for development or proof-of-concept use - you'd swap this for a database/redis in production. For more information, see the store contract
Organizations
For most apps that don't have the idea of organizations, you can leave organizationMode disabled, which will mean you can only have user-level settings.
However for apps that do we have two additional primitives which are enabled by turning that config option on; org and org-user.
These do what you'd expect - apply a setting against an entire org (org) or against a user within an org (org-user).
Categories are walked in the following order:
org-useruserorgdefault(if specified)
Settings Config
Each setting has a key, a schema, and a default value, along with what categories of identity it applies to, and metadata. These are set statically in your application — there's no dynamic registration. For the full picture (including the defineRegistry helper and a few compile-time gotchas), see Defining settings.
import { z } from "zod";
import {
createSettings,
createInMemoryStore,
} from "@theshoregroup/settings-core";
const settings = createSettings({
organizationMode: true,
store: createInMemoryStore(), // swap for a real adapter in production
registry: {
"notif_new-login-location": {
categories: ["user"] as const,
schema: z.boolean(),
default: true,
meta: {
name: "New login location notifications",
description: "Email me when I sign in from a new device.",
},
},
"auth_allowed-email-domains": {
categories: ["org"] as const,
schema: z.array(z.string()),
default: [] as ReadonlyArray<string>,
meta: { name: "Allowed email domains", description: "…" },
},
"notif_supplier-update": {
categories: ["org", "org-user"] as const,
schema: z.enum(["all", "mentions", "none"]),
default: "all" as const,
meta: { name: "Supplier update notifications", description: "…" },
},
},
});Usage
Identity
When using this package you'll need to provide an identity, which must resolve a userId, orgId as required. You should create an adapter that resolves these values from your authentication provider.
const identity = { userId: "u_123", orgId: "o_456" };
const { value, source } = await settings.resolve("notif_supplier-update", {
identity,
});Methods
We provide a set of methods for resolving and mutating settings. They all mostly follow the same pattern and should work the way you expect.
resolve(key, { identity, only? })
const { value, source } = await settings.resolve("notif_supplier-update", {
identity,
only: "org", // optional: restrict to a single category
});update(key, value, { category, identity })
Writes a value at a specific category. Note that update(key, null) stores a real null, it doesn't delete the row — use reset for that.
await settings.update("notif_supplier-update", "mentions", {
category: "org-user",
identity,
});reset(key, { category, identity })
Deletes the stored value at a category, falling back to the next link in the chain.
await settings.reset("notif_supplier-update", {
category: "org-user",
identity,
});updateDynamic / resetDynamic
Runtime-key variants of update/reset, for when the key, value, or category come from the wire and can't be narrowed to literals.
await settings.updateDynamic(req.body.key, req.body.value, {
category: req.body.category,
identity,
});list({ identity, filter? })
Resolves every setting the identity can see, optionally filtered by category or keys.
const all = await settings.list({ identity });
const orgSettings = await settings.list({ identity, filter: { category: "org" } });purge(args)
Bulk-deletes stored values for an identity scope, e.g. on account deletion or org offboarding.
await settings.purge({ category: "user", userId: "u_123" });scope(identity)
Bundles an identity so you don't have to pass it to every call.
const scoped = settings.scope(identity);
const all = await scoped.list();Further Reading
- Defining settings — how and why to write a
registry entry, the
defineRegistryhelper, and the literal-tuple gotcha. - Concepts — domain model, resolution chain, identity,
null semantics (including the
update(k, null)vsresettrap). - Store contract — implementing a custom
SettingsStore(Drizzle/Prisma/Kysely/etc.). - Errors — errors thrown by the library
License
MIT — see LICENSE.
