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 .hash file. Both are wrong, and the second one publishes the secret.
What's actually happening
#/Config is 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.docly defining the fields, and the document itself at #/Config.docly bound to that schema. The schema gives the customer a labelled form with descriptions, marks the required fields, and can use Type: "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 #/Config the right home for a secret key, and it is why a secret must never be written into a .hash page, 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 config global — #config.CF_Turnstile_Site_Key#. Verified 2026-08-26: that global was undefined in a hash template while docly.getJson("#/Config") returned the fields correctly on the same request. Use getJson, 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.docly
Ordinary Input fields. Use Type: "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."
} Name is the data key you read in code; Field is 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, getJson returns null, 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>/Config Must 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.