Put configuration and secrets in the Config document
-
What you'll see
A site needs a value that is not content and not code: an API key, a Turnstile secret, a tracking id, a feed endpoint, a per-environment toggle. It has to be readable by templates and API functions, editable without a deploy, and — for the secret half of it — unreachable from the web. The tempting move is a JSON file somewhere convenient, or a constant in a
.hashfile. Both are wrong, and the second one publishes the secret. -
What's actually happening
#/Configis the standard location for site configuration in Docly. It is a schema-bound document in the#/tree, which means three things at once: it is never served over HTTP, it is editable through a generated form rather than by hand-editing JSON, and its fields are readable from every template and API function.This is not one option among several. A workspace that scatters configuration across ad-hoc JSON files has no single place to look, no form to edit it in, and no guarantee about what is exposed.
It is a document with a schema, not a loose file
Two pieces: a schema at
#/Schemas/<Name> config.doclydefining the fields, and the document itself at#/Config.doclybound to that schema. The schema gives the customer a labelled form with descriptions, marks the required fields, and can useType: "password"so a secret is not displayed in plain text while editing.Secrets belong here, and only here
Anything under
#/is unreachable over HTTP — see Keep private data under the # folder. That is what makes#/Configthe right home for a secret key, and it is why a secret must never be written into a.hashpage, a stylesheet, or a JSON file in a published folder.Public values live here too. A Turnstile site key is public by design and is written into the markup — but it still belongs in
#/Config, so the customer changes both keys in one place rather than hunting through templates.Reading it
Read the document with
docly.getJson("#/Config"). This works in both hash templates and API functions and returns the fields as a plain object.Some documentation and examples use a
configglobal —#config.CF_Turnstile_Site_Key#. Verified 2026-08-26: that global wasundefinedin a hash template whiledocly.getJson("#/Config")returned the fields correctly on the same request. UsegetJson, or verify the global with a scratch page before relying on it — an unresolved name in a hash expression renders empty rather than failing, so a wrong guess here silently produces a form with no site key. -
What to do
1. The schema —
#/Schemas/<Site> config.doclyOrdinary
Inputfields. UseType: "password"for secrets and put a real description on each one — the customer will be the one editing this.{ "ObjectVariant": "Input", "Required": true, "Type": "password", "Name": "CF_Turnstile_Secret_Key", "Field": "Cloudflare Turnstile - secret", "Description": "Secret. Read only by #/API/Turnstile.js. Never goes in markup." }Nameis the data key you read in code;Fieldis the label the customer sees. Not the other way round.2. The document —
#/Config.docly{ "Schema": "<Site> config", "Document": { "CF_Turnstile_Site_Key": "0x4AAAAAAA...", "CF_Turnstile_Secret_Key": "0x4AAAAAAA..." }, "Tags": [], "EmbeddedFiles": [] }3. Reading it
// In a hash template — public values only #{ var cfg = docly.getJson("#/Config") || {}; }# <div class="cf-turnstile" data-sitekey="#docly.htmlAttributeEncode(cfg.CF_Turnstile_Site_Key)#"></div> // In an API function — where the secret is used var cfg = docly.getJson("#/Config") || {}; var secret = cfg.CF_Turnstile_Secret_Key;Always guard with
|| {}. Before the document exists,getJsonreturnsnull, and property access on it throws in an API function and renders empty in a template.Verify it is not served
curl -s -o /dev/null -w "%{http_code}\n" https://<site>/ConfigMust return
404. Run it after creating the document, not from memory.Checklist
- Configuration lives in
#/Config— not in ad-hoc JSON files, not as constants in templates. - A schema backs it, so the customer gets a form with labels and descriptions.
- Secrets use
Type: "password"and are read only where they are used. - Read with
docly.getJson("#/Config"), guarded with|| {}. /Configreturns 404.
- Configuration lives in
Last reviewed 26 Aug 2026