Services
This page configures the optional services a deployment can add beside its databases: a cache, a search engine, webhooks, and one email service.
{ "connections": [ { "name": "shared", "kind": "cache", "url": "valkeys://cache.internal:6379", "auth": { "password": "${VALKEY_PASSWORD}" } }, { "name": "search", "kind": "search", "url": "https://search.internal:8108", "auth": { "apiKey": "${TYPESENSE_API_KEY}" } }, { "name": "mail", "kind": "email", "url": "smtps://smtp.example.com:465", "from": "CRM <[email protected]>", "auth": { "username": "${SMTP_USER}", "password": "${SMTP_PASSWORD}" } }, { "name": "dealer-hooks", "kind": "webhook", "url": "https://hooks.dealer.example/tablewalk", "auth": { "bearer": "${DEALER_HOOK_TOKEN}" } } ]}None is a source of truth, and a deployment that names none runs the same
without them. They are not browsed or offered to agents. Credentials go in
auth as ${VAR} references, never in the URL, and are never logged. Search
and webhooks are preview.
The cache ("kind": "cache")
Section titled “The cache ("kind": "cache")”A Valkey server, or anything that speaks its protocol (Redis included), shared by every instance of a deployment.
| Field | Meaning |
|---|---|
url |
valkeys://host:6379 (TLS) or valkey://host:6379; rediss:// and redis:// too. /2 after the port picks a database. |
auth |
{ "password": "${VALKEY_PASSWORD}" }, plus "username" for an ACL user. |
insecure |
true allows plain valkey:// / redis:// on a trusted network. |
allowLinkLocal |
As on an HTTP connection. |
It holds cached API answers for an HTTP connection with "cache": { "ttl": 60 },
and the rate-limit windows of public forms, so
instances behind a load balancer spend one budget. When the cache does not
answer, reads miss and rate limits fall back to this process’s own count; the
outage is said on stderr at most once a minute.
The search engine ("kind": "search")
Section titled “The search engine ("kind": "search")”A Typesense server.
| Field | Meaning |
|---|---|
url |
The server’s base URL; https unless insecure. |
auth |
{ "apiKey": "${TYPESENSE_API_KEY}" }. |
collectionPrefix |
Collections are <prefix>_<app>_<resource>; default tablewalk. |
insecure, allowLinkLocal |
As on an HTTP connection. |
An App names which resources are searched, and by which columns:
resources: { contact: { access: 'write', search: { connection: 'search', fields: ['name', 'company', 'email'] } },},A contains filter on a searched field — the list’s search box, a reference
picker, the palette — asks the engine for up to 250 keys, best first, then
reads those rows from the database under the reader’s grants, so rows and
counts agree. When the engine is down the filter runs on the database and the
reader is told once.
The App re-indexes each record after its own writes. For writes made
elsewhere, and after first declaring search, run
tablewalk --app <dir> --config <file> --reindex [resource].
Not yet: tenant Apps, a secondary source, or a reference path such as
owner_id.name. The resource needs a single-column primary key.
Webhooks ("kind": "webhook")
Section titled “Webhooks ("kind": "webhook")”A webhook is where a command’s effects
go after it commits: post('dealer-hooks', …). The App names it; the config
says where it is.
- https, or plain http only to
localhost,127.0.0.1or[::1]. Redirects are refused and link-local addresses are never reached. - A credential in
auth:{ "bearer": "${TOKEN}" }or{ "header": "x-signature", "value": "${SECRET}" }.
Each delivery is a POST of JSON:
{ "effect": "notifyDealer", "command": "convertLead", "receipt": "<receipt id>", "occurredAt": "2026-09-13T12:00:00.000Z", "payload": { … } }with an Idempotency-Key header that is the same on every attempt of one
effect; deduplicate by it. A 2xx is delivered. Anything else is retried with
backoff (1 s doubling to five minutes, eight attempts), then fails until an
operator redrives it with redriveEventDeliveries from
tablewalk/maintenance; readiness reports effects: work_failed.
Provision the delivery store once, with the server stopped:
provisionSqliteEffects(path, { appId, commands }) or
provisionPostgresEffects(url, { appId, commands }). A new command version or
effect is a new destination, provisioned the same way.
Email ("kind": "email")
Section titled “Email ("kind": "email")”The one email service a server sends through: SMTP over TLS, or log: to
write each message instead. What it sends, and how a notice’s email waits for
its write to commit: email.
Docker Compose
Section titled “Docker Compose”examples/docker/docker-compose.yml starts the cache and the search engine
under a services profile: docker compose --profile services up, with
VALKEY_PASSWORD and TYPESENSE_API_KEY from your shell or an .env file.