DB-backed config hierarchy: instance/org/brand/prompt run config, encrypted credentials, and plan enforcement#483
Open
jrhizor wants to merge 20 commits into
Open
DB-backed config hierarchy: instance/org/brand/prompt run config, encrypted credentials, and plan enforcement#483jrhizor wants to merge 20 commits into
jrhizor wants to merge 20 commits into
Conversation
…t in env validation
…nd history-metered run budgets
…from the db hierarchy
…, and document in-app config
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
# Conflicts: # packages/lib/src/providers/registry/oxylabs.ts
The admin routes import getEffectiveConfigFn/setConfigValuesFn and the instance-config entity fns, so those modules are in the client graph. Their *Impl functions used @/lib/auth/helpers (getRequestHeaders) outside a createServerFn handler, so the server-only import survived tree-shaking and the vite build failed import protection. Move the impls behind .server.ts siblings referenced only from handler bodies; tests import them directly.
Raw U+0000 bytes made git treat run-policy.ts as binary, hiding the whole module from diff review. Use \u0000 escapes instead — the produced key string is byte-identical, so history metering and every test are unchanged.
The env->DB import is a one-time legacy migration (advisory-locked, stamped by instance_meta) and is unit-tested in the lib config import tests. e2e already seeds model_targets directly, so the standalone config-import step only kept SCRAPE_TARGETS alive as a permanent fixture. Drop it.
- ResolvedConfig gains concrete per-key value types (TypedEntry<T>), deleting the as-number/as-string[] casts in the resolver and its consumers. - One shared rowSelectorKind ladder feeds row ranking, matching, provenance, and the write-time selector check (was spelled out four times). - selectConfigRows collapses the four near-identical fetch builders' tail. - resolvePromptTargets accepts an injected assignablePoolUsage so a batch caller can compute the org-scoped pool count once instead of per prompt. - Drop the unused getPropertyForKey; soften the defaults-never-stored note to admit the two per-prompt assignment-signal keys.
- listProviderCredentials gates inside the impl (was the one read whose auth sat in the wrapper, leaving the exported impl callable ungated). - setConfigValues resolves the write actor once via requireConfigWrites instead of re-fetching session + member row per entry. - instance-config validators + inferred input types move to a pure instance-config.schemas module: the wrapper imports schemas, the impl imports types — no more wrapper↔impl circular type import or hand-kept interfaces. - Drop the now-redundant cadence value casts (resolved config is typed).
- process-prompt fetches brand+competitors together and run-history + org-usage together, and counts the assignable pool once per org per batch (via resolvePromptTargets' injected usage) instead of once per prompt. - schedule-maintenance resolves per-org entitlements with Promise.all rather than a serial await-in-loop on every tick. - Document the org-budget cross-job TOCTOU as an accepted soft cap.
- Extract the structured-research maxUses/context-size and OpenAI tracked context size into named constants next to the tracked-run caps, so the intentional 2-vs-5 gap is visible and edits can't miss a call site. - VERSION_REQUIRED_PROVIDERS becomes one exported set (was an inline || chain plus a duplicate Set); the client route keeps a documented mirror.
The keyId was written but never read; verify it so a key mismatch surfaces a precise cause instead of an opaque GCM auth failure, matching its comment.
This was referenced Jul 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #8 — the configuration-hierarchy foundation the rest of the cloud project builds on.
What
One uniform configuration hierarchy (instance → organization → brand → prompt) stored in the database, replacing
SCRAPE_TARGETSas the source of truth for what runs, how often, and with which credentials. Mode differences (cloud / whitelabel / local / demo) are write-policy data over one schema, not code branches.configs— the single cascading table: one row per (scope, selector, key). Every key is declared once in a code registry (zod schema, default, allowed scopes/selectors, merge rule, permission class); defaults are never stored — the DB holds only overrides (the Sentry/Discourse pattern). Precedence: scope (prompt > brand > org > instance), then selector specificity (target > model > none); entitlement ceilings clamp last. Every resolved value carries provenance.model_targets— the catalog: platform name → implementation (provider/version/webSearch),priorityreserved for provider failover,requiredEntitlementfor custom-plan-only targets. Selections stay model-name-keyed, so an implementation can move without touching any selection.provider_credentials— provider keys stored encrypted (AES-256-GCM, fresh IV per operation, AAD-bound, fail-closed) under a new optionalELMO_ENCRYPTION_KEY. Env credentials remain first-class: a DB row is authoritative for its provider, otherwise env. Thesecret-refsource ships in the schema for the Infisical follow-up. Plaintext never reaches the client.organization_settings—planKey+ entitlement overrides, staff-only (Stripe subscription billing via @better-auth/stripe #345 swaps the plan source to Stripe; this stays the custom-plan lever).credentials-unready,not-in-plan-menu,pool-exhausted, …) surfaced in the UI, so "why isn't claude running?" is a badge, not a support thread.Enforcement
Two distinct guard classes:
MAX_PROMPTSgap server-side and in API v1), picks ⊆ standard menu and ≤ pick count, Claude pool counted over enabled prompts including the enable transition. All provably inert outside cloud (unlimited entitlements short-circuit before any query).promptRunshistory (per-target runs/day and an org-wide assignable-model budget), so assign → run → unassign → reassign cannot multiply spend. Per-request caps land with it (from the superseded Per-target replication/cadence for SCRAPE_TARGETS and hard API spend caps #452): output-token caps on all four API providers, Anthropicmax_uses: 1, OpenAImaxToolCalls, OpenRouter native-first web plugin withmax_results+search_context_size.Authorization is per-key: registry permission class →
CONFIG_POLICY[mode][class]→ the only write gates server functions use. Whitelabel members keep full content-config access (brand settings/prompts/competitors — unchanged); run config there is operator-admin-only and the prompt level is off entirely. Demo is read-only at every level. The local bootstrap user is now promoted to instance admin (previously/adminwas unreachable in local mode). Cloud follows the #344 packaging. A 160-cell registry-driven policy matrix plus per-mode contract suites pin all of this — any permission change is a visible test diff.Migration
brands.delayOverrideHours/brands.enabledModelsinto brand-scope rows before dropping the columns (safe today: no environment does rolling deploys — compose migrates before new code starts, cloud is pre-launch).SCRAPE_TARGETSimport on first boot (per-entry parsing, invalid entries skipped with warnings). With unchanged env, effective targets and run volume are identical — asserted by tests. Afterward the DB owns config; a single deprecation warning fires if env diverges. The web app renders a "no targets configured" state pre-import and never imports.SCRAPE_TARGETS: the seed plants the stub catalog row and stampsinstance_meta(so the worker's boot import can't pull the CLI-driver's real API targets), and a dedicated CI step exercises the env→DB import path end-to-end.elmo initgeneratesELMO_ENCRYPTION_KEY; the first realelmo upgrademigration backfills it on existing installs (keyed to the published 0.2.17). PostgreSQL ≥ 15 is now a documented requirement (the CLI compose ships 16-alpine).UI
New admin section: Targets (catalog with credential-readiness status), Providers (write-only key entry, verify, hints), Defaults (instance cadence/replication/onboarding target with provenance and clear-to-default), Organizations (cloud-only plan assignment). The brand LLMs page is finally editable — model checklist writing
run.enabled_models(its first-ever write path), plan pick meter, exclusion-reason badges. Per-prompt models card with Claude base/web mode and a pool meter (cloud/local only). Zero new dependencies anywhere in the PR.Issue linkage
getEntitlements, per-org overrides); Stripe price-ID wiring stays with Stripe subscription billing via @better-auth/stripe #345.:xN:Chenv syntax never ships) and admin /providers page and dynamic llms settings #199 (admin providers UI).brands.organizationId; nothing depends on thebrand.id == org.idartifact.Tests
933 across the repo, all green: lib 367 (resolver precedence/clamp property grid, crypto tamper matrix, import idempotency), config 31, web 472 (policy matrix, enforcement, credential no-plaintext-egress), worker 35 (dueness, budgets, churn, legacy volume equivalence), cli 28. Typecheck clean in web/worker/cli/lib/config; docs validated with a full www build.
Follow-ups
Infisical
secret-refresolver · per-targetrequestPolicyplumbing into provider request bodies · in-app first-boot setup wizard · org browse/plan-assignment UI (ships as load-by-id) · cost estimator panel · #345 Stripe wiring.Note: migration 0011 is not executed by this PR; it applies through the normal db-migrate flow.