18. App configuration
A maintenance banner every visitor sees, a switch that hides the export button and blocks the export function, changed by cron functions and administrators, cached by the client.
Some things about Shelf should change without a deploy: a maintenance notice while the operators work, a feature that has to be switched off now. This chapter adds app configuration: a declared set of values that administrators and functions change at runtime, that apps read through one endpoint, and that rules can consult so a switch holds on the server too.
Declare it
Create config/shelf/app-config/schema.json: a JSON Schema whose every value has a default, so the app always gets a complete configuration.
examples/config/shelf/app-config/schema.json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"properties": {
"maintenance": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean", "default": false, "description": "Show the banner" },
"message": { "type": "string", "maxLength": 200, "default": "" }
}
},
"features": {
"type": "object",
"additionalProperties": false,
"properties": {
"export": {
"type": "object",
"additionalProperties": false,
"properties": {
"enabled": { "type": "boolean", "default": true, "description": "Members may export their assets" }
}
}
}
}
}
}And config/shelf/app-config/rules.yaml: who may read what. A key with no rule, on itself or an ancestor, can’t be read by apps at all.
examples/config/shelf/app-config/rules.yaml
# Who may read what (chapter 18). A path with no rule, here or on an ancestor,
# is not readable by apps.
read:
maintenance: "true" # everyone, without signing in: the banner
features: user != nil # signed-in membersThe banner is public ("true": even without a session); the features group needs a signed-in member. Restart backd: it refuses to start on a value without a default, an object that doesn’t close its properties, or a rule for a path the schema doesn’t declare.
Read it in the app
await backd.config.load(['maintenance', 'features'])
backd.config.getSync('maintenance.enabled', false)
backd.config.on('change', (paths) => render(paths))
The client keeps a copy per user, fresh for the server’s cache_ttl (5 minutes unless app_config.cache_ttl in realm.yaml says otherwise) with no request at all; after that it serves the copy and revalidates it in the background with If-None-Match, which usually costs a 304 and no body. Signing out drops the copies, so the next person on the browser never sees what the previous one could read. The app shows the maintenance banner from maintenance and the Export my assets button from features.export.enabled, which a visitor can’t read at all (the button isn’t there signed out).
A switch the server enforces
Hiding a button protects nothing: anyone can call the function. The export function puts the switch in its invoke rule, where the server reads it for each call:
examples/config/shelf/main/_functions/export/function.yaml
# Lists the caller's own assets as one JSON document (chapter 18).
#
# The switch is enforced here, in the invoke rule: with features.export.enabled
# off, the server refuses the call whatever the app shows. `appConfig()` reads
# the configuration once for the request.
invoke: user != nil && appConfig("features.export.enabled") == true
rate_limit:
per: user
limit: 10
window: 1mWith features.export.enabled off, the call is refused with 403 whatever the app shows. The same appConfig("features.export.enabled") works in collection rules, where it is a constant to the query, so a rule that depends on it is still a database filter.
Change it
An administrator changes values with the CLI (or PATCH /_admin/app-config, or the client’s admin API):
backd app-config set --realm shelf --key features.export.enabled --value false
backd app-config get --realm shelf
backd app-config history --realm shelf
backd app-config rollback --realm shelf --version c3
Each change is validated as a whole against the schema, applied atomically on the version you read (a concurrent change answers 412), kept in the history with its actor and every path’s old and new value, and audited. A rollback is itself a new version.
A maintenance window is two cron functions
Time-based changes need nothing built in: scheduled functions with config: write do them, and the history names the function as the actor.
examples/config/shelf/main/_functions/maintenance-open/function.yaml
# Opens and closes the maintenance window (chapter 18): two scheduled functions
# that change app configuration, which is all a window is. `config: write` is
# what lets a function do that; its changes are recorded with the function as
# the actor. Internal: nobody calls it over HTTP; `backd functions invoke`
# runs it by hand.
internal: true
mode: async
schedule: "0 2 * * 6" # Saturdays at 02:00 UTC
config: writeexamples/config/shelf/main/_functions/maintenance-open/index.ts
// maintenance-open: shows the banner (chapter 18). The input is for runs by hand;
// the schedule runs it without one.
import { defineFunction } from "@backd/functions";
export default defineFunction("main/maintenance-open", async (ctx) => {
const { message } = (ctx.input ?? {}) as { message?: string };
const out = await ctx.admin.config.set({
"maintenance.enabled": true,
"maintenance.message": message ?? "Maintenance in progress: some things may be slow.",
});
return { version: out.version, changed: out.changed };
});maintenance-close is the same with ctx.admin.config.reset(["maintenance"]), which returns the group to its defaults. backd functions invoke --function shelf/main/maintenance-open runs one by hand, as the tour does.
You should see
- Opening the window (by the function) shows the banner to a signed-out visitor, and closing it hides it again.
- Turning
features.export.enabledoff hides the Export my assets button for members and makes theexportfunction answer403; turning it back on restores both. backd app-config historylists each change withfunction:main/maintenance-openor the administrator as the actor, androllbackrestores an earlier state as a new version.- A second read of the configuration with the
ETagit returned answers304.
The tour (make shelf-tour) checks all of it. Read App configuration for the reference: the schema’s limits, per-path rules, the admin API, the functions’ ctx.config and the client’s cache.