Supasheet.

App Configuration

A key-value store for app-wide settings, exposed to the client with per-row visibility control

Overview

supasheet.configs is a simple key-value table for settings that describe the app itself rather than any one schema's data — things like the display name shown in the header, a public description, feature flags, or other small values the frontend needs before a user even signs in.

create table supasheet.configs (
  id bigint generated by default as identity primary key,
  key text not null unique,
  value jsonb,
  description text,
  is_public boolean not null default false
);
ColumnPurpose
keyUnique identifier, e.g. app.name, app.description
valueArbitrary JSON payload
descriptionHelper text (shown in admin tooling)
is_publicWhether anonymous (signed-out) clients may read this row

Visibility Model

Unlike most supasheet tables, configs is readable directly by anon and authenticated via RLS — there's no role_permissions entry required:

create policy configs_select_anon on supasheet.configs for
select
  to anon using (is_public);

create policy configs_select_authenticated on supasheet.configs for
select
  to authenticated using (true);
  • Anonymous users only ever see rows where is_public = true.
  • Authenticated users see every row, public or not.

Only put values in configs that are safe to expose to any signed-in user. If a setting needs to be admin-only, gate it the normal way — a dedicated table with role_permissions — rather than relying on is_public = false, since every logged-in user can still read it.

Writes aren't exposed through PostgREST at all — INSERT/UPDATE/DELETE are revoked from every client role. Change config values via migration or through the database directly.

Reading Config in the App

The frontend queries only the public rows, since app name/description need to render before authentication resolves:

const { data } = await supabase
  .schema("supasheet")
  .from("configs")
  .select("key, value")
  .eq("is_public", true)

This is wrapped by appConfigQueryOptions() (src/lib/supabase/data/config.ts) and consumed via the useAppConfig() hook, which currently resolves two keys — falling back to sensible defaults if either is missing:

export interface AppConfig {
  name: string
  description: string
}
KeyUsed for
app.nameHeader/sidebar branding, page titles
app.descriptionMeta description, empty-state copy

Adding a Setting

insert into supasheet.configs (key, value, description, is_public) values
  ('app.name', '"Acme Admin"'::jsonb, 'Displayed in the header and browser tab', true),
  ('app.description', '"Internal operations console"'::jsonb, 'Used for meta tags', true);

Remember to run select supasheet.refresh_metadata(); after any schema change in the same migration (not required here since configs rows aren't DDL, but do it if you also touched tables/comments) — see Database Schema.

Next Steps

  • Authorization — how role_permissions gates everything other than configs
  • Database Schema — where configs sits among the other supasheet schema objects

On this page