Wiring Catalog Services
Redis, NATS, ClickHouse, Meilisearch, Qdrant — most backing services wire the same way. This is the generic loop; auth, storage, workflows, and observability have dedicated runbooks that override it (Auth, Storage, Temporal, SigNoz).
1. Discover — don't guess
kilter catalog # list available services
kilter catalog show <service> # category, provides (env vars), detect (client lib)
kilter add <service> # add to kilter.yaml and re-render
kilter env # resolved connection strings + portsprovides tells you which env vars the service publishes into your app container; detect names the client library to install. Ports are deterministic per project but different across projects — always read them from kilter env, never hardcode.
2. Connect by category
| Category | Env | Pattern | Client lib |
|---|---|---|---|
| Database (postgres) | DATABASE_URL | connection string → ORM init | drizzle / prisma / pg |
| Cache (redis) | REDIS_URL | connection string → client init | ioredis / bullmq |
| Analytics DB (clickhouse) | CLICKHOUSE_URL + admin Secret | resolve password → client; batch inserts | @clickhouse/client |
| Messaging (nats, kafka, rabbitmq) | NATS_URL / KAFKA_BROKERS | broker URL → producer/consumer | nats / kafkajs / amqplib |
| Search (qdrant, meilisearch, typesense) | <SVC>_URL (+ key Secret for some) | API URL → client; create index in app code | per service |
| Email (mailpit) | SMTP_HOST, SMTP_PORT | SMTP → mail client | nodemailer |
Most services need no credentials in dev, but some (clickhouse, meilisearch) keep an admin password or master key in a Kubernetes Secret rather than in provides. If kilter env doesn't surface a credential the client needs, read the service's Secret with kubectl get secret ... -o jsonpath=... (using the project kubeconfig at ~/.cache/kilter/<name>/kubeconfig) and put it in .env.local — never commit it.
3. Validate
Wiring is not done until a test proves connectivity.
kilter doctor # kilter.yaml ↔ code dependency alignment
kilter exec -- <health check> # service reachable from inside the app containerThen write a minimal connectivity test under tests/wiring/<service>.test.ts:
// redis example — the shape varies by category
const redis = new Redis(process.env.REDIS_URL!);
expect(await redis.ping()).toBe('PONG');
await redis.quit();Finish by confirming existing pages still load.
4. Document
Add the new env vars to .env.local.example (no secrets), and a line to the project docs if the service changes the dev workflow.
Inline custom services
Anything not in the catalog can be declared directly in kilter.yaml — an image, a port, and the env vars it should publish:
services:
- postgres # catalog reference
- name: imgproxy # inline custom service
image: darthsim/imgproxy:latest
port: 8080
provides:
IMGPROXY_URL: "http://imgproxy.{{ .Namespace }}.svc:8080"The provides template renders with the project namespace and injects into the app container like any catalog service. Then kilter up picks it up.
Anti-patterns
- Hardcoded URLs or ports — they're project-specific; read from
kilter env. - Direct browser calls to cookie-based services — auth needs the same-origin proxy; see Auth: Wiring Ory.
- Skipping validation — a service that starts is not a service that's wired.