One Checked Value Instead of Two Booleans: Making a Bad Config Unrepresentable
Two on/off flags allow a state with no correct behaviour: both off. Storing the enabled set as one CHECK-constrained value and validating the resulting state instead.
The problem: two switches, four states, one of them nonsense
ResearchForge can analyse papers with two AI providers, Anthropic and Groq. The owner chooses which is primary, the other becomes the automatic fallback, and the owner can also switch a provider off entirely. The obvious storage for "is each provider enabled" is two boolean settings. Two booleans have four combinations, and one of them, both off, has no correct behaviour: every analysis would fail. This is a short design note on keeping that state out of the database rather than relying on code to remember to check for it.
Store the set, and constrain the set
Owner settings live in a key/value table. Instead of two boolean rows, there is one row, enabled_ai_providers, holding the set of enabled providers, with a CHECK constraint that lists every legal value and has no empty option:
ALTER TABLE system_settings ADD CONSTRAINT system_settings_enabled_providers
CHECK (
key <> 'enabled_ai_providers'
OR value IN ('anthropic', 'groq', 'anthropic,groq')
);Preventing "both off" across two rows would need a trigger, or application code that every writer remembers to call. A single value with a closed list makes the bad state unrepresentable: the database refuses it, including from a hand-edited row in the Supabase dashboard. The constraint carries a comment saying exactly that, so the next person to read the schema knows the empty set is excluded on purpose.
The constraint is keyed on key, so other settings in the same table are unaffected. It was added alongside the earlier constraint that limits the primary provider to two values, rather than rewriting it; PostgreSQL applies both, so neither can weaken the other. The migration inserts the default, both enabled, with ON CONFLICT DO NOTHING, so applying it changes no behaviour and never overwrites an owner's choice.
Validate what the configuration would become
One rule spans two rows and the database does not enforce it: the primary provider must be one of the enabled ones. That lives in the API, and the way it is checked matters.
Validating each field on its own would reject a perfectly sensible request: "make Claude the primary and switch Groq off". Taken one at a time, switching Groq off while it is primary is invalid. So the endpoint first computes what the configuration would become from the current values and the request, then checks the invariants against that resulting state:
- At least one provider stays enabled. (The CHECK already guarantees this; the API checks too so it can explain itself.)
- The primary is in the enabled set. If not, the refusal names the fix: make the other provider primary first, then disable this one.
Only then are the changed values written. An honest caveat: the primary and the enabled set are two rows written by two separate requests, not one transaction. A failure between the two writes could leave a combination the API would have refused, and nothing in the database prevents it. The empty set is impossible; this second rule is only as strong as the code path that writes it.
Enforced where it is used, too
The same idea continues into the code that uses the setting. The provider router is built per request from the enabled set, and a disabled provider is never constructed, so no branch inside the router can reach it, as primary or as fallback. A disabled primary is reported as a configuration error, because nothing can run. A disabled fallback is not an error at all; the analysis runs on the primary alone and, if the primary fails, its own error surfaces instead of the work moving silently to a vendor the owner excluded.
A test asserts that a disabled provider's call count is zero even when the primary fails with an error that would normally trigger the fallback. How the fallback itself is scoped is in When Not to Fall Back to Another AI Provider.
The pattern is not specific to AI providers. When a pair of flags has a combination your system cannot handle, store the combination as one value, constrain it in the database, and validate the resulting state rather than each field. More on the system is in the ResearchForge case study.
The work behind this
Related
Keep reading
When Not to Fall Back to Another AI Provider
Automatic failover between model vendors is easy to build and easy to build wrong. Most errors should never trigger it.
When the Code Ships Before the Migration: Degrading One Schema Level at a Time
Selecting a column that a pending migration adds made PostgREST fail every library read. The fix degrades one migration tier at a time, so deploy order stops mattering.
One Pydantic Schema, Two LLM Vendors: Native Parsing vs JSON Mode
The same Pydantic models drive structured output from Anthropic's native parsing and Groq's JSON mode. Forbidding extra fields, catching truncation, and validating instead of repairing.