Skip to content
Metric VaultHelp Center
Open app

Runtime configuration

Every platform_config key the code reads, with its accepted values, default, exact effect, blast radius and the risk of setting it wrong.

Last updated 2026-08-06

Summary#

platform_config is the platform's runtime settings store: a single D1 table of key and value strings that the worker reads on every request through a 60-second cache. Fourteen key shapes are read by code. Everything else you can write into the table is inert. This page is the complete registry, with defaults, effects and the risk of each.

Overview#

Storage. One table: platform_config (key TEXT PRIMARY KEY, value TEXT, updated_at INTEGER, updated_by TEXT). updated_at is unix seconds and updated_by is the email of the owner who wrote it. D1 is used rather than KV because a Pages deploy resets KV, and a configuration store that forgets on deploy is worse than no store at all.

Reading. mvConfigAll(env) loads the whole table into a Map and caches it for 60,000 ms. Individual reads go through mvConfig(env, key, fallback). The read fails open: if D1 is unreachable the map comes back empty, every default applies and every tool stays on. Writing or deleting a key resets the cache immediately, so a change is live at once rather than up to a minute later.

Writing. Only an owner can write. POST /api/admin/config/set takes { token, key, value }, or { token, key, delete: true } to remove a key. A blank key returns 400 {"error":"key required"}; a non-owner returns 403 {"error":"Only an owner can change config."}. Values are stored as strings, with null becoming the empty string.

There is no key allow-list and no value length limit. Any key you type is accepted and stored. A typo therefore creates a dead key that looks like a setting and does nothing, which is the single most common configuration mistake here.

Auditing. Writes record config.set with the first 120 characters of the value as meta; deletions record config.delete. Both carry the key as the target. See Audit log.

The key registry#

Fourteen key shapes are read by code. Nothing else in the table has any effect.

KeyWritten byAccepted valuesDefault when absentBlast radius
tool_off:<toolType>Tools & Cache, Turn off, or by hand1 disables. Any other value is treated as ononOne AI tool, all customers
suspended:<email>Customers screen1 suspendsnot suspendedOne customer
cache_days_defaultTools & Cache, Apply to allA positive number of dayseach call site's own lifetimeEvery tool's provider cache
cache_days:<toolId>Tools & Cache, the lifetime selectA positive number of daysnoneOne tool's provider cache
ai_reco_min_planConfig screen, AI Recommendations accessstarter, pro, agency, enterpriseproWho may press Get Recommendations
ai_reco_quota:<plan>Config screen, AI Recommendations accessA whole number of 0 or morethe built-in per-plan defaultMonthly recommendation allowance for that plan
plan_quota:<plan>Config screen, Plans & limitsA whole number of 0 or more0 / 100 / 500 / 2000 / 10000 by planThat plan's monthly premium credits: the quota gate and every usage display
seat_limit:<plan>Config screen, Plans & limitsA whole number of 0 or more0 / 4 / 14 / unlimited by planHow many members that plan can invite
premium_thresholdConfig screen, Plans & limitsA whole number from 1 to 1003Which credit costs the monthly quota gates, all plans
tool_cost:<toolId>Tools & Cache, the credits pickerA whole number from 0 to 1000the built-in credit costWhat one run of that tool charges, all customers
light_rate_per_hourConfig screen, Plans & limitsA whole number from 10 to 100000100The hourly fair-use cap on light tools, signed-in users and anonymous IPs alike
hero_scans_per_dayConfig screen, Plans & limitsA whole number from 0 to 10002How many homepage teaser scans one visitor IP gets per day
quota_bonus:<email>:<month>Customers screen, GrantA positive whole number0Extra credits for one customer, that calendar month only
maintenance_messageConfig screen, Maintenance modeFree textnoneA dismissible banner in the admin console and at the top of the customer dashboard

The email in suspended:<email> is always lower-cased. The plan in ai_reco_quota:<plan>, plan_quota:<plan> and seat_limit:<plan> is a plan id in lower case, and the tool id in tool_cost:<toolId> is lower-cased too.

Key detail#

tool_off:<toolType>#

Set the value to 1 to take one premium AI tool out of service. The tool type is the internal tool id, for example tool_off:ai_visibility.

The check runs at the start of the premium AI route, before quota and metering, and returns 503 {"error":"This tool is temporarily unavailable. Please try again shortly."}. The customer is not charged, because the return happens before usage is incremented.

Risk. Three things catch people out.

  • The key is matched against the request's tool type verbatim. A typo disables nothing and produces no warning.
  • Requests that can be answered from the shared cross-customer result cache return before this check runs, so a heavily cached tool can keep serving results for a while after you disable it. Clear that tool's cached data if the disable must be immediate. See Tools and cache management.
  • Growth Actions is dispatched to its own handler before this check, so tool_off:growth_actions has no effect.

Delete the key, or set it to anything other than 1, to re-enable the tool. The switch lives on Tools and cache management: Turn off and Turn on in the Actions column write and delete this key with a confirmation. It can still be typed by hand in the Set a config value card.

suspended:<email>#

Set by the Suspend button on the Customers screen and deleted by Reactivate. A value of 1 blocks that customer at the AI quota gate and at the AI recommendation gate with 403 {"error":"This account is suspended. Please contact support.","code":"account_suspended"}.

The same key drives the suspended badge on the Customers screen, the status column of the subscriber directory, and the bulk suspension scan that the directory runs (SELECT key FROM platform_config WHERE key LIKE 'suspended:%' AND value='1').

Risk. Writing it by hand with the wrong casing or a stray space silently fails to suspend anyone, and it will not show as suspended in the UI either. Use the Customers screen, which lower-cases the address and busts the cache for you. Addresses in the worker's dev-unlimited list return before the suspension check and cannot be suspended at all. See Customer lookup and billing.

cache_days_default#

The global lifetime, in days, for cached provider responses across every tool. Written by Apply to all on the Tools & Cache screen, which also deletes every per-tool override so nothing shadows it. Absent, each tool uses the lifetime its own code passes.

Risk. A long value lowers the provider bill and makes every tool's data staler at once, including tools where freshness matters most, such as rank tracking. A short value raises the bill across the whole platform. The value is parsed defensively: anything unparseable or not greater than zero falls back to the caller's own lifetime, so a bad value degrades freshness rather than correctness.

cache_days:<toolId>#

A per-tool lifetime in days that beats cache_days_default. Written by the lifetime select on the Tools & Cache screen; selecting the default option deletes the key.

Risk. The override only binds for tools whose fetcher passes its tool id into the cache layer. The Tools & Cache screen already knows which those are and shows the rest as Set via Apply to all. Writing cache_days:<toolId> by hand for a tool the screen marks that way produces a key that never applies. Full detail in Tools and cache management.

ai_reco_min_plan#

The lowest plan allowed to press Get Recommendations on any tool result. The built-in default is pro. Accepted values are starter, pro, agency and enterprise; the value is lower-cased on read, and an unrecognized value falls back to the rank of pro.

Below the minimum, the customer gets 403 with the code reco_upgrade_required and a message naming both the required plan and their own. Free is always below every accepted minimum, so Free is always blocked.

Risk. Lowering it to starter opens a paid AI feature to every Starter account, metered only by ai_reco_quota:starter. Raising it takes the feature away from paying customers with no warning and no notification.

ai_reco_quota:<plan>#

The monthly recommendation allowance for one plan. Parsed with parseInt; a blank value or an unparseable one is ignored and the default applies. Zero is a valid value and blocks the plan entirely.

PlanBuilt-in default
starter30
pro100
agency400
enterprise2000
unlimitedunlimited, metering skipped

Free has no entry and therefore resolves to 0, which does not matter in practice because Free is blocked by the minimum-plan gate first. Over quota the customer gets 429 with the code reco_quota_exceeded and a message naming the quota and the reset date. The counter is ai_reco_runs on the current month's usage row and it is incremented before the upstream call, so concurrent requests cannot all pass the check.

Risk. These are the direct control on AI spend for this feature. A large value on a low-priced plan converts straight into provider cost. Setting a value for a plan that does not exist creates a dead key.

plan_quota:<plan> and seat_limit:<plan>#

Set from the Plans & limits card. plan_quota overrides how many premium credits the plan gets per month; seat_limit overrides how many team members the plan may invite (the owner is not counted). Both are read by the quota gate, the dashboard usage numbers, the Customers screen and the team endpoints in the same place, so the enforcement and every display move together.

All four of the new commercial keys fail safe: a missing, blank or unparseable value means the built-in default applies, and the internal unlimited tier is never overridable. A value of 0 is real: plan_quota:starter of 0 stops Starter running premium tools at all.

Risk. The pricing page and the comparison table state the built-in numbers as static copy. If you change a quota or seat limit here, production behaviour changes immediately but the marketing pages keep saying the old numbers until someone edits them. Update Plans and pricing and the pricing page in the same change.

premium_threshold#

The credit cost at or above which a tool is gated by the monthly quota. Below it, a tool is never blocked by the quota (the Free plan still only runs cost-0 tools, and the hourly light-tool rate limit still applies). Raising it makes more tools effectively unlimited for paying customers; lowering it to 1 makes nearly every tool count against the quota. Accepted range 1 to 100.

tool_cost:<toolId>#

Set from the credits picker on Tools and cache management. Overrides what one run of that tool charges, for every customer, at every charge site: the quota gate, the usage increment, the cache-hit charge and the saved-result record all read the same override. 0 makes the tool free to run. The dashboard's run buttons state the built-in costs in their labels, so an override makes those labels stale until the dashboard copy is updated; the actual charge follows the override.

light_rate_per_hour and hero_scans_per_day#

Set from the Fair-use limits row of the Plans & limits card. light_rate_per_hour is the hourly cap the light-tool rate limiter applies per signed-in user, and per IP for the anonymous free-tools endpoints; the 429 it returns states the effective number. hero_scans_per_day caps the anonymous homepage teaser scan per visitor IP and per UTC day; 0 turns the teaser off for anonymous visitors entirely. Both fail safe to their defaults. The customer-facing limits reference states the defaults as fixed numbers, so a lasting change here should update All limits and quotas too.

quota_bonus:<email>:<month>#

Written by Grant on Customer lookup and billing; repeated grants accumulate into the same key. The amount is added on top of the customer's plan quota in the quota gate and in both usage displays, so the dashboard shows the raised total. The month key is part of the config key, so the bonus expires silently when the month rolls over, and granting again next month writes a fresh key. Deleting the key from the Configuration table revokes the bonus immediately.

maintenance_message#

Free text, written by the Maintenance mode card. Ticking the box with an empty text field writes the default string Site is undergoing maintenance. Please check back soon. Unticking it deletes the key.

The dashboard fetches the message from GET /api/status/banner (public, no auth, nothing but this one string) and renders it as a dismissible banner above the app. A customer's dismissal lasts for their browser session and is per message, so changing the text shows the banner again. The config cache means a new message can take up to a minute to appear.

Note

Note: The banner warns; it does not block. Tools keep running while it is up. To actually take a tool out of service, use the availability toggle on Tools and cache management.

Writing and deleting keys#

POST /api/admin/config/set

FieldRequiredNotes
tokenyesSupabase access token, in the JSON body
keyyesTrimmed. Blank returns 400 {"error":"key required"}
valuenoStored as a string. null becomes ""
deletenotrue deletes the row and skips value entirely

POST /api/admin/config lists every row ordered by key. It is admin-gated, unbounded and unpaginated, so the Configuration table on screen is the whole table.

The Config screen#

Admin console → Operations → Config

CardRoleWhat it does
Social OAuth readinessany adminMeant to show which social platforms have OAuth credentials configured
Set a config valueownerThe raw key and value editor. Messages: Enter a key. and Saved.
Maintenance modeownerWrites or deletes maintenance_message
AI Recommendations accessownerWrites ai_reco_min_plan and the four ai_reco_quota:<plan> keys. A blank limit sends a delete. Validation message: Monthly limits must be a whole number of 0 or more. Success: Saved. Changes take effect immediately.
Configuration tableany admin to read, owner to deleteKey, Value, Updated, By and a red Delete per row. Empty reads No config set. Defaults apply.
Screenshot
The Config screen showing the Set a config value card, the Maintenance mode card, the AI Recommendations access card and the Configuration table.
Note

Note: The Social OAuth readiness card requests a diagnostic endpoint without the internal secret it requires, so it currently always renders No platforms configured in code. regardless of the real state. It is a display defect, not a security problem. Check OAuth credentials from the environment variables instead. See What the admin console cannot do yet.

Risk summary#

MistakeWhat happensHow you notice
Typo in a key nameA dead row that looks like a settingNothing changes; the row sits in the table
tool_off: on a tool served from the shared cacheThe tool keeps answering for cached queriesRuns continue in the Run Log
Editing cache_days: by hand for a shared-fetcher toolThe override never bindsThe Tools & Cache row still reads default
Large cache_days_defaultStale data across every tool at onceCustomers report old numbers
ai_reco_quota: set highAI provider spend risesThe provider cost tile on System Health
ai_reco_min_plan raisedPaying customers lose a feature silentlySupport tickets, no notification is sent
Deleting cache_days_defaultEvery tool reverts to its built-in lifetimeProvider cost changes

Nothing in this table notifies anyone. Every write is audited, so the audit log is the only record of who changed what and when.

See also

Was this article helpful?