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
);| Column | Purpose |
|---|---|
key | Unique identifier, e.g. app.name, app.description |
value | Arbitrary JSON payload |
description | Helper text (shown in admin tooling) |
is_public | Whether 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
}| Key | Used for |
|---|---|
app.name | Header/sidebar branding, page titles |
app.description | Meta 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_permissionsgates everything other thanconfigs - Database Schema — where
configssits among the othersupasheetschema objects