Configure the SDK
The SDK takes its settings from your code, the environment and a .quilletrc file, in that order, and it takes the delivery key from the environment alone.
Why settings are split between file and environment
The same site code runs in more than one place. A laptop reads a local CMS, a preview deployment reads staging, and production reads prod. Settings that change from one place to the next belong in the environment. Settings that stay the same belong in a file the team commits. The key's secret belongs nowhere that someone opening the repository could read it. The SDK draws those lines for you, and it refuses a configuration that would cross them, such as a secret written into the file.
If you are a content manager, the setting that matters to you is the environment. A site configured for staging shows what has been published in staging, and nothing you publish in prod reaches it.
How the four layers build one configuration
quillet() and quilletNext() build their configuration from four layers. A layer higher in this list wins over every layer below it:
- The object you pass to
quillet()orquilletNext(). - The environment variables
QUILLET_API_URL,QUILLET_PROJECT,QUILLET_ENV,QUILLET_LOCALE,QUILLET_KEY_IDandQUILLET_KEY_SECRET. - One configuration file: the one
QUILLET_CONFIGnames, or else the first of.quilletrc,.quilletrc.json,quillet.config.js,quillet.config.mjsandquillet.config.tsfound in the working directory or a directory above it. - The defaults.
createClient from @quilletcms/sdk skips all four. It takes a complete configuration object and reads nothing else.
| Setting | Meaning | Default |
|---|---|---|
apiUrl |
The CMS's origin, scheme and host only: https://cms.example.com. A path is refused. |
none, required |
project |
The project's slug. | none, required |
env |
The environment's key, such as prod. The delivery key must be bound to it. |
none, required |
locale |
The locale every read asks for unless the call names one. | the project's default locale |
refs |
How many levels of references delivery inlines, 0 to 5. | 3 |
cache.ttl |
Seconds the client answers a repeated read from memory without asking the CMS. | 0 |
assets.mode |
proxy: a private file's url points at your own site. signed: it is the CMS's signed link. |
proxy |
assets.path |
The path on your site the file proxy is mounted at. Starts with /, does not end with one. |
/media |
assets.proxyPublic |
Send public files through the proxy too, so no CMS host appears in your HTML. | false |
assets.maxAge |
Seconds in the proxy's Cache-Control: private, max-age= for a private file. |
300 |
retry.attempts |
How many times a read is retried after a 429 or a 503. | 2 |
Any string in the file may hold ${VAR}. When the SDK reads the file, it replaces that with the environment variable of the same name. An unset variable is an error. So is ${QUILLET_KEY_ID}, ${QUILLET_KEY_SECRET} or ${QUILLET_MANAGEMENT_KEY}, because credentials are read from the environment directly and are never copied into a setting. The SDK refuses a key in the file, and it refuses a top-level key the table does not list, with one exception: sync, which belongs to the Quillet CLI. The SDK leaves that section alone, since the two tools share one .quilletrc. Inside assets, an unknown option is refused too. Inside cache and retry it is not. A misspelt cache: { tll: 30 } or retry: { attempt: 5 } is ignored without a word, and the default stays in force.
A problem with the file itself stops the load, and the SDK reports these one at a time, each as its own ConfigError. They are a QUILLET_CONFIG that names no file, a file that is not valid JSON or cannot be loaded, an unset ${VAR}, a key, and an unknown top-level key. Once the file has been read, the SDK checks the merged settings and reports every problem it finds together, in one ConfigError, before it makes any request. Here is what a client says when it is built with a project slug in capitals, a path on apiUrl, refs: 9 and no environment or key:
Quillet configuration is not usable — missing: env (QUILLET_ENV), key.id (QUILLET_KEY_ID), key.secret (QUILLET_KEY_SECRET) — set those variables, or put apiUrl, project and env in the config file; apiUrl must be an origin like https://cms.example.com — check QUILLET_API_URL or the quillet() call; project must be a slug of lowercase letters, digits and dashes, got "Handbook"; refs must be an integer from 0 to 5, got 9
Caching and retries
Each client keeps its own store in memory. The store holds one entry per request URL, and once it reaches 1,000 entries it drops the least recently used. With the default cache.ttl of 0, every read still goes to the CMS, but as a conditional request that carries the last ETag. If the document has not changed, the CMS answers 304 with no body, and the client hands back what it stored. With a ttl above 0, a read made within that many seconds is answered from memory, and the CMS is not asked at all. The client never stores a preview read or a read pinned with at. invalidate('docs/intro') forgets one document in every locale, and invalidate() forgets everything.
When a read meets a 429 or a 503, the client waits and tries again, up to retry.attempts times. It waits as long as Retry-After says, or else 250 ms, doubling each time and never more than five seconds. Any other failure is thrown at once.
The Next.js client turns this store off and uses Next's data cache instead. Next.js covers how.
How to
Commit the settings that do not change between machines in .quilletrc, at the root of the site:
{ "apiUrl": "https://cms.example.com", "project": "handbook", "locale": "en-US", "assets": { "path": "/media" } }
Set the environment and the key wherever the code runs. On a laptop that is the shell. On a host it is the host's secret store:
export QUILLET_ENV=<env>
export QUILLET_KEY_ID=<keyId>
export QUILLET_KEY_SECRET=<secret>
The key is a delivery key bound to that environment. Ask a key bound to prod for staging and the CMS answers 403, "This key cannot read that environment.", which the SDK throws as an UnauthorizedError.
Settings that need logic go in quillet.config.ts instead, as its default export. If you type the object as ConfigOverrides, a misspelt option shows up while you write it:
// quillet.config.ts: a local CMS on a laptop, the real one everywhere else.
import type { ConfigOverrides } from '@quilletcms/sdk/node'
const config: ConfigOverrides = {
apiUrl: process.env.NODE_ENV === 'production' ? 'https://cms.example.com' : 'http://localhost:3000',
project: 'handbook',
refs: 2,
}
export default config
The SDK loads this module synchronously, which is what lets you call quillet() at the top of a module. It also means the file cannot use top-level await, and that a .ts file needs a Node that runs TypeScript itself: 22.18 or later.
To override any layer for one client, pass it an object. This client reads German and keeps answers for thirty seconds:
// german.ts: one client with its own locale and a short cache.
import { quillet } from '@quilletcms/sdk/node'
export const de = quillet({ locale: 'de-DE', cache: { ttl: 30 } })
It is better to fail at start-up, with the whole list of problems, than at the first request. This script refuses to start when the configuration cannot work:
// start.ts: refuse to start with a configuration that cannot work.
import { ConfigError } from '@quilletcms/sdk'
import { quillet } from '@quilletcms/sdk/node'
try {
const q = quillet()
console.log(`reading ${q.config.project}/${q.config.env} from ${q.config.apiUrl}`)
} catch (error) {
if (error instanceof ConfigError) {
console.error(error.message)
process.exit(1)
}
throw error
}
You can log q.config safely. The key is on it, but it is not enumerable, so console.log(q.config) and JSON.stringify(q.config) leave it out.
When something outside the client tells you a document changed, drop that document from the store so the next read fetches it again:
// refresh.ts: forget one document, then everything.
import { quillet } from '@quilletcms/sdk/node'
const q = quillet({ cache: { ttl: 300 } })
await q.get('docs/intro')
q.invalidate('docs/intro')
q.invalidate()
Limits
- The key is read from
QUILLET_KEY_IDandQUILLET_KEY_SECRET, or from code. No file can hold it. - One client reads one project and one environment. To read two environments you need two clients and two keys.
invalidate(key)forgets only that document's own reads. Listings, trees and searches that include it stay stored until theirttlruns out orinvalidate()clears everything.- The store lives in the process's memory. Two server instances keep two separate stores, and a restart empties both.
retrycovers 429 and 503 only. A request that never reaches the CMS is thrown as aNetworkErrorat once, with no retry.apiUrlcannot carry a path, so there is no way to configure a CMS served under a sub-path such ashttps://example.com/cms.
Related
- Overview and install: the three entry points.
- Next.js: the data cache, tags and revalidation that replace the store.
- Configure the CLI: the
syncsection of the same.quilletrc. - Access: which key is for what.
- Environments: creating one, and keys per environment.
- Delivery API: the
ETag,Cache-Controland status codes the client relies on.