# Settings

> Per-user and per-organization settings with a Standard Schema per key, defaults, typed get and set, and matching pg_jsonschema checks.

Source: https://bettersupabase.com/docs/blocks/settings

The `settings` block stores preferences as key-value rows: one table for each
user's own settings and one for organization settings. You declare the keys
once with a Standard Schema each (Zod, Valibot, ArkType), and the block gives
you typed `get`, `set` and `reset` that validate before they write.

```bash
better-supabase sql add settings   # adds tenant and access as well
```

| Table                   | Key                      | Who reads                    | Who writes                     |
| ----------------------- | ------------------------ | ---------------------------- | ------------------------------ |
| `user_settings`         | `(user_id, key)`         | the user                     | the user                       |
| `organization_settings` | `(organization_id, key)` | members with `settings.read` | members with `settings.update` |
| `platform_settings`     | `key`                    | each key's `read` rule       | each key's platform permission |

The default roles give `admin` both permissions and `member` the first.
Rename them with `sql.modules.settings.permissions`. Both tables keep
`updated_by` and `updated_at`, and the functions run as the caller, so the
row level security policies decide every read and write.

## Declaring settings [#declaring-settings]

```ts title="src/lib/settings.ts"
import { toStandardJsonSchema } from "@valibot/to-json-schema";
import { defineSettings } from "better-supabase/blocks/settings";
import * as v from "valibot";

export const settings = defineSettings({
  user: {
    theme: {
      schema: toStandardJsonSchema(v.picklist(["light", "dark", "system"])),
      default: "system",
    },
    digest: { schema: v.object({ weekly: v.boolean() }) },
  },
  organization: {
    defaultRole: {
      schema: toStandardJsonSchema(v.picklist(["member", "viewer"])),
      default: "member",
    },
  },
});
```

A key without a `default` reads as `undefined` until it is set. A stored
value that no longer matches its schema (after you narrow an enum, for
example) also reads as the default, so old rows never break a page.

## Reading and writing [#reading-and-writing]

```ts title="app/settings/actions.ts"
import { rpcTransport } from "better-supabase/blocks/settings";

import { settings } from "@/lib/settings";

const client = settings.connect({
  transport: rpcTransport(supabase, { schema: "api" }),
});

const theme = await client.user.get("theme").orThrow(); // "light" | "dark" | "system"
await client.user.set("theme", "dark").orThrow();
await client.user.reset("theme"); // back to the default

const all = await client.organization.get(organizationId).orThrow();
await client.organization.set(organizationId, "defaultRole", "viewer");
```

`set` returns a `validation` error with the schema's issues when the value
doesn't match, and an `invalid_input` error for a key you didn't declare;
neither reaches the database. Use `sqlTransport(postgres.asUser(claims))`
instead of `rpcTransport` to call the functions over Postgres. With
`rpcTransport`, set `sql.modules.settings.api` to an exposed schema such as
`api` and pass `rpcTransport(supabase, { schema: "api" })`; see
[SQL modules](/docs/blocks/sql#calling-a-module-over-the-data-api).

## Platform settings [#platform-settings]

Settings for the whole product, such as fee rates, routing rules or an
announcement text an admin console edits, go in the `platform` scope. Each
key can name the platform permission (`is_platform`) that may change it and
who reads it: `public` (also signed out), `authenticated` (the default) or
`staff` (holders of the key's permission). Keys guarded by different
permissions share one table:

```ts title="lib/platform-settings.ts"
export const platformSettings = defineSettings({
  platform: {
    invoiceFeePercent: {
      schema: v.pipe(v.number(), v.minValue(0), v.maxValue(10)),
      default: 0,
      permission: "platform.billing.manage",
      read: "public",
    },
    aiDefaults: {
      schema: v.object({ model: v.string() }),
      permission: "platform.ai.manage",
      read: "staff",
    },
  },
});
```

Pass the definition as `sql.modules.settings.options.schemas`, so the
module writes the permission and read rule of each key into the policies.
When `schemas` lists platform keys, only those keys can be written: a key
the config doesn't list is refused, so a typo or a stale key never lands in
`platform_settings`. Rows that exist without a listed key are read with
`options.platform.permission` (default `settings.manage`, renamed with
`permissions.platform`) and `options.platform.read`. Without listed keys,
any key can be written with that permission.

```ts
const admin = platformSettings.connect({
  transport: sqlTransport(postgres.asUser(claims)),
});
await admin.platform.set("invoiceFeePercent", 1.5).orThrow();
const fee = await admin.platform.get("invoiceFeePercent").orThrow();
```

A key a caller may not read comes back as its default, and a write without
the key's permission fails with `forbidden`.

## Checks in the database [#checks-in-the-database]

Pass the definition to the module, and keys whose schema implements
[Standard JSON Schema](https://standardschema.dev/json-schema) get a
`pg_jsonschema` check, so writes that skip TypeScript are held to the same
shape. Zod 4 schemas implement it directly; wrap Valibot schemas in
`toStandardJsonSchema`:

```ts title="better-supabase.config.ts"
import { defineConfig } from "better-supabase/config";

import { settings } from "./src/lib/settings.ts";

export default defineConfig({
  sql: {
    modules: {
      settings: { options: { schemas: settings } },
    },
  },
});
```

```bash
better-supabase sql add jsonb-schemas settings
```

The checks need the `jsonb-schemas` module, which installs `pg_jsonschema`.
Each one is named `bs_json_value_<key>` and applies only to rows with that
key. `options.schemas` also takes plain `{ user, organization }` maps of key to
JSON Schema. Keys whose schema has no JSON Schema (`digest` above) are
validated in TypeScript only.

## Options [#options]

| Option     | Default                                                    | What it does                                                                   |
| ---------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `schemas`  | none                                                       | A `defineSettings()` result or `{ user, organization, platform }` JSON Schemas |
| `platform` | `{ permission: "settings.manage", read: "authenticated" }` | The permission and read rule of platform keys that don't set their own         |