catalog-info.yaml Reference
Complete field-by-field reference for configuring your service. This file lives in your project root and is the single source of truth for how the builder deploys your service.
Minimal Example
The smallest valid catalog-info.yaml that will deploy successfully:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: my-service
description: My service description
annotations:
insureco.io/framework: express
spec:
type: service
lifecycle: production
owner: my-orgThis deploys an Express API on a nano pod tier (256MB RAM, 5 tokens/hour) with no databases, a catch-all route, and auto-generated Dockerfile.
metadata
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Service name. Must be lowercase alphanumeric + dashes. This becomes your hostname: <name>.tawa.insureco.io |
description | string | Yes | Brief description of what this service does |
tags | string[] | No | Tags for categorization (passed to Koko but not enforced) |
annotations (metadata.annotations)
All annotations use the insureco.io/ prefix. These control how the builder generates your Dockerfile and configures your deployment.
| Annotation | Values | Default | What it does |
|---|---|---|---|
insureco.io/framework | express | nextjs | hono | fastify | static | worker | unknown | Determines Dockerfile template, build/start commands, and output directory |
insureco.io/node-version | Any Node.js version string | 20 | Node.js base image tag in Dockerfile |
insureco.io/pod-tier | nano | small | medium | large | xlarge | nano | RAM allocation and hosting gas rate. See Gas & Wallets |
insureco.io/port | Integer (as string) | 3000 (or 80 for static) | Container port — EXPOSE in Dockerfile, PORT env var |
insureco.io/health-endpoint | URL path | /health or /api/health | K8s liveness/readiness probe path. Must return 200. |
insureco.io/build-command | Shell command | npm run build | Custom build command in Dockerfile RUN step |
insureco.io/start-command | Shell command | Framework default | Custom CMD in Dockerfile |
insureco.io/output-dir | Directory path | dist or .next | Build output directory copied to production stage |
insureco.io/env-vars | Comma-separated list | (empty) | Build-time env vars injected as Dockerfile ARG/ENV.Next.js only — required for NEXT_PUBLIC_* variables. Has no effect on other frameworks (Express, Hono, Fastify, etc.). |
insureco.io/copy-paths | Comma-separated paths | (empty) | Directories to copy from the builder stage to the production image. Enables monorepo support — when set, the builder skips the separate deps stage and relies on build-command for all dependency installation. Replaces output-dir. |
insureco.io/homepage | URL | (empty) | Landing page URL for your service (web app homepage or API docs). Shown on the Bio-ID Connected Services page and registered in Koko. Per-module overrides are supported via spec.modules[].homepage. |
insureco.io/framework: express, the builder uses node dist/index.js as the start command and dist as the output directory. For nextjs, it uses node server.js (standalone mode) and .next. Override any default with the explicit annotation.Monorepo example (copy-paths)
For projects with multiple sub-packages (e.g., an Express API + a Vite dashboard), use copy-paths to tell the builder which directories belong in the production image:
metadata:
annotations:
insureco.io/framework: express
insureco.io/build-command: "cd api && npm ci --omit=dev && cd ../dashboard && npm ci && npm run build && mkdir -p ../dash/dist && cp -r dist/* ../dash/dist/"
insureco.io/start-command: "node api/src/app.js"
insureco.io/copy-paths: "api,dash/dist"The build-command handles installing dependencies for each sub-package and building the frontend. The copy-paths annotation tells the builder to copy only api/ (with its node_modules) and dash/dist/ (the built frontend) into the final image.
spec (required fields)
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Always service |
lifecycle | string | Yes | production | experimental | deprecated |
owner | string | Yes | Organization slug (e.g., insureco). Must match your Bio-id org slug. Used to derive wallet ID: wallet-<owner> |
spec.owner field must exactly match your organization slug in Bio-id. This is how the deploy gate finds your wallet. If it does not match, you will get a "wallet not found" error during deployment.spec.databases
Declare the databases your service needs. The builder automatically provisions connection secrets and injects environment variables into your pod.
Supported types
| Type | Env var injected | Connection string format |
|---|---|---|
mongodb | MONGODB_URI | mongodb://<host>:27017/<name>-<environment> |
redis | REDIS_URL | redis://<host>:6379/0 |
neo4j | NEO4J_URI | bolt://<host>:7687 |
Example
spec:
databases:
- type: mongodb
- type: redisThis creates two K8s secrets and injects MONGODB_URI and REDIS_URL into your pod. In your application code, read them from process.env:
// In your Express app
import mongoose from 'mongoose'
const uri = process.env.MONGODB_URI
if (!uri) throw new Error('MONGODB_URI not configured')
await mongoose.connect(uri)Database naming
By default, the MongoDB database name is <service-name>-<environment>. For example, a service named my-api deployed to production gets my-api-production. You can override this with the name field:
spec:
databases:
- type: mongodb
name: shared-platform-dbpostgres, mysql) will fail during tawa preflight validation.What the builder creates
For each database entry, the builder:
- Creates a K8s Secret named
<service>-db-<type>(e.g.,my-api-db-mongodb) - Injects the env var via Helm
--set - The connection string appears in your pod as
process.env.MONGODB_URI
Database provisioning is idempotent — deploying again does not create duplicate secrets or databases.
spec.routes
Declare your HTTP routes. These are registered in Koko (the service registry) and used by Janus (the API gateway) for routing and gas metering.
Route fields
| Field | Type | Required | Description |
|---|---|---|---|
path | string | Yes | URL path (e.g., /api/users). Supports :param syntax. |
methods | string[] | Yes | GET, POST, PUT, DELETE, PATCH |
auth | string | No | required | optional | none (default: none) |
description | string | No | Human-readable description of the endpoint |
scopes | string[] | No | Required scopes for this route (future enforcement) |
gas | integer | No | Gas tokens charged per successful API call through Janus. Set 0 for free routes. Omit to use the platform default (1 token). See API Gas. |
Example
spec:
routes:
- path: /api/policies
methods: [GET, POST]
auth: required
description: Policy CRUD
gas: 3 # 3 tokens per call
- path: /api/policies/:id
methods: [GET, PUT, DELETE]
auth: required
gas: 1 # 1 token per call
- path: /api/reports/generate
methods: [POST]
auth: required
gas: 10 # expensive operation
- path: /api/health
methods: [GET]
auth: none
gas: 0 # free — no gas chargedHow routes work with Janus
When a service calls your API through the gateway (api.tawa.insureco.io), Janus:
- Looks up the service in Koko by path
- Validates the HTTP method is allowed
- Checks auth if the route requires it
- Checks the caller's wallet has enough gas
- Proxies the request to your pod
- On a 2xx response, debits the caller's wallet and credits yours
Gas pricing
The gas field controls how many tokens are charged per successful API call. Janus resolves gas cost using a three-level fallback:
- Route-level: The
gasvalue in yourcatalog-info.yaml(recommended) - Platform table: Hardcoded fallback for legacy services not yet migrated
- Platform default: 1 token if nothing else is configured
Set higher gas on compute-intensive routes (PDF generation, AI processing) and lower gas or 0 on simple reads. View your gas pricing with tawa gas pricing. See Gas & Wallets for full details on gas metering, settlement, and CLI commands.
spec.routes entirely, the builder registers a catch-all route: / [GET, POST, PUT, DELETE] with no auth and the platform default gas cost (1 token).spec.dependencies
Declare other Tawa services your app needs to call. Every dependency requires a Bio-ID scope grant — there are no unauthenticated pod-to-pod connections. The builder handles OAuth client provisioning, scope grant requests, and credential injection automatically.
Fields
| Field | Type | Required | Description |
|---|---|---|---|
service | string | Yes | Target service name (as registered in Koko) |
scopes | string[] | Yes | OAuth scopes to request (e.g. [relay:send]) |
transport | string | Yes | direct (K8s internal DNS, no gas) or gateway (through Janus, gas metered) |
Example
spec:
dependencies:
- service: relay
scopes: [relay:send]
transport: direct # pod-to-pod K8s DNS, no gas
- service: raterspot
scopes: [raterspot:rate]
transport: gateway # through Janus, gas meteredTransport modes
direct | gateway | |
|---|---|---|
| Network path | K8s internal DNS (pod-to-pod) | Through Janus gateway |
| Gas metered | No | Yes |
| Scope grant required | Yes | Yes |
| Auth on each request | Bearer token (OAuth client_credentials) | Bearer token (Janus validates) |
| URL injected | {SERVICE}_URL (K8s DNS) | (none — call Janus URL directly) |
How resolution works
For each dependency, the builder:
- Creates a Bio-ID scope grant request for the declared scopes
- Same-org dependencies are auto-approved; cross-org requires owner approval
- On approval, adds scopes to your service's OAuth client
- For
directtransport: resolves K8s DNS URL via Koko, injects{SERVICE}_URL - For
gatewaytransport: consumer calls the Janus URL with a Bearer token
| Dependency | Transport | Env var | Example value (production) |
|---|---|---|---|
relay | direct | RELAY_URL | http://relay.relay-prod.svc.cluster.local:3000 |
raterspot | gateway | (none) | Call via api.tawa.insureco.io |
In your code, authenticate with OAuth client_credentials and use the injected URL:
// Get a token with the granted scopes
const tokenRes = await fetch(BIO_ID_URL + '/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.BIO_CLIENT_ID,
client_secret: process.env.BIO_CLIENT_SECRET,
scope: 'relay:send',
}),
})
const { access_token } = await tokenRes.json()
// Call the target service
const res = await fetch(process.env.RELAY_URL + '/api/relay/send', {
headers: { 'Authorization': `Bearer ${access_token}` },
// ...
})Legacy fields
The old internalDependencies and externalDependencies fields are still supported during the migration window:
internalDependencies→ mapped todependencieswithtransport: direct(no scope check during migration)externalDependencies→ mapped todependencieswithtransport: gateway
Migrate to spec.dependencies at your earliest convenience. See Service Dependencies & Scopes for the full guide.
spec.auth
Controls how your service authenticates users.
spec:
auth:
mode: sso # sso | service-only | none| Mode | Description | OAuth client? |
|---|---|---|
sso | Users authenticate via Bio-id. Best for web apps with user login. | Yes — auto-provisioned |
service-only | Service-to-service auth only. No user-facing login. | Yes — auto-provisioned |
none | No auth. Public APIs or internal-only services. | No |
Auto-provisioned OAuth
When your service has routes (or is set to SSO mode), the builder automatically creates an OAuth client in Bio-id:
- Client ID:
<service-name>-<environment> - Redirect URI:
https://<hostname>/api/auth/callback - K8s Secret:
<service-name>-oauthwithBIO_CLIENT_IDandBIO_CLIENT_SECRET
These are injected into your pod automatically. Use them in your auth flow to exchange codes for tokens with Bio-id.
spec.config
Declare the environment variables your service needs. This makes your catalog-info.yaml the single source of truth for what config a service requires, which values are secrets, and what defaults to apply.
Declaration fields
| Field | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Environment variable name. Must be a valid env var (letters, digits, underscores, starting with a letter or underscore). |
secret | boolean | No | If true, values are encrypted at rest (AES-256-GCM) and never returned by the API. Default: false |
required | boolean | No | If true, the deploy blocks if this key has no value set and no default. Default: false |
default | string | No | Fallback value injected during deploy if no explicit value is set. Defaults have the lowest priority. |
Example
spec:
config:
- key: NEXTAUTH_SECRET
secret: true
required: true
- key: STRIPE_API_KEY
secret: true
required: true
- key: LOG_LEVEL
default: "info"
- key: APP_NAMEHow it works
- On deploy: The builder reads your declarations, validates that all
requiredkeys have a value (or default), and caches them on the service record. - Preflight check: If any required key is missing, the deploy fails before the Docker build, saving time and compute.
- Defaults: Keys with a
defaultvalue are injected automatically during deploy with the lowest priority — explicit config and secrets always override defaults.
Setting values
Two ways to provide config values:
# Option 1: Push from your .env file (recommended)
# Secrets are auto-detected from your declarations
tawa config push
# Or from a specific file
tawa config push .env.production
# Option 2: Set individually
tawa config set LOG_LEVEL=debug
tawa config set --secret STRIPE_API_KEY=sk_live_xxxWhen you use tawa config push, the builder classifies each key against your declarations — keys marked secret: true are automatically encrypted, plain keys are stored as config, and any key not declared in your catalog is rejected.
tawa config set to set values directly. After the first deploy, tawa config push becomes available.tawa preflight to check if all required config is set before deploying. The deploy also runs this check automatically.Full Example
A production-ready catalog-info.yaml with all features:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: platform-api
description: Insurance platform core API
tags:
- api
- platform
annotations:
insureco.io/framework: express
insureco.io/node-version: '20'
insureco.io/pod-tier: medium
insureco.io/port: '3000'
insureco.io/health-endpoint: /api/health
insureco.io/build-command: npm run build
insureco.io/start-command: node dist/server.js
insureco.io/output-dir: dist
spec:
type: service
lifecycle: production
owner: my-org
databases:
- type: mongodb
- type: redis
routes:
- path: /api/policies
methods: [GET, POST]
auth: required
description: Policy CRUD
gas: 3
- path: /api/policies/:id
methods: [GET, PUT, DELETE]
auth: required
gas: 1
- path: /api/quotes
methods: [GET, POST]
auth: required
gas: 5
- path: /api/health
methods: [GET]
auth: none
gas: 0
dependencies:
- service: iec-wallet
scopes: [wallet:read]
transport: direct
config:
- key: NEXTAUTH_SECRET
secret: true
required: true
- key: LOG_LEVEL
default: "info"
auth:
mode: ssoWhat this creates
| Resource | Value |
|---|---|
| Pod tier | medium — 1 GB RAM, 23 tokens/hour |
| Deploy gate reserve | 23 × 730 = 16,790 tokens required |
| K8s secrets | platform-api-db-mongodb, platform-api-db-redis, platform-api-oauth |
| Env vars | MONGODB_URI, REDIS_URL, IEC_WALLET_URL, BIO_CLIENT_ID, BIO_CLIENT_SECRET |
| Routes registered | 4 routes in Koko with gas pricing (0–5 tokens/call), accessible via Janus gateway |
| Production URL | platform-api.tawa.insureco.io |
Environment Variables
The builder injects environment variables in this order of precedence (later values override earlier ones):
- Catalog defaults: Values from
spec.config[].default(lowest priority) - Database vars:
MONGODB_URI,REDIS_URL,NEO4J_URI - Service discovery vars:
<SERVICE>_URLfrom dependencies (direct transport) - OAuth vars:
BIO_CLIENT_ID,BIO_CLIENT_SECRET - Managed config: Values set via
tawa config setortawa config push - Managed secrets: Encrypted values (set via
--secretor auto-detected from catalog declarations)
You can set env vars with the tawa config command:
# Push all vars from your .env file (recommended)
# Secrets auto-detected from spec.config declarations
tawa config push
# Or set individual values
tawa config set API_KEY=abc123
tawa config set --secret STRIPE_KEY=sk_live_xxx
# Pull config to .env.local (for local dev)
tawa config pull
# List all config
tawa config listCommon Mistakes
1. Missing or wrong framework annotation
# WRONG: missing framework, builder uses "unknown"
metadata:
annotations: {}# CORRECT: explicit framework
metadata:
annotations:
insureco.io/framework: expressWithout a framework, the builder cannot generate a Dockerfile and may use incorrect defaults.
2. Wrong database type
# WRONG: "postgres" is not supported
spec:
databases:
- type: postgres# CORRECT: only mongodb, redis, neo4j
spec:
databases:
- type: mongodbUnsupported types will fail during tawa preflight validation.
3. spec.owner does not match your org
# WRONG: "my-team" is not a Bio-id org slug
spec:
owner: my-team# CORRECT: use your actual org slug
spec:
owner: john-doe # matches your Bio-id orgThe deploy gate looks up wallet-<owner>. If the owner does not match your org slug, the wallet is not found and deployment fails. Check your org slug with tawa whoami.
4. Health endpoint does not exist
# catalog says /api/health but your app only has /health
annotations:
insureco.io/health-endpoint: /api/healthK8s probes will fail and your pod enters CrashLoopBackOff. Make sure the health endpoint path in your catalog matches a real route in your application.
5. Port mismatch
# catalog says 3001 but your app listens on 3000
annotations:
insureco.io/port: '3001'The builder sets PORT=3001 in the container and K8s routes traffic to port 3001. If your app listens on 3000, the health probe fails and the pod restarts. Make sure these match.
6. Next.js missing standalone output
// next.config.js — missing output: 'standalone'
module.exports = {}// next.config.js — correct
module.exports = { output: 'standalone' }The Next.js Dockerfile expects standalone output. Without it, the Docker image is missing files and the app crashes on startup.
7. Using env-vars for non-Next.js frameworks
# WRONG: env-vars has no effect on Express/Hono/Fastify
metadata:
annotations:
insureco.io/framework: express
insureco.io/env-vars: "VITE_API_URL"The insureco.io/env-vars annotation only generates Dockerfile ARG/ENV directives for the Next.js framework. For other frameworks, the annotation is silently ignored and the variables will be undefined at build time. If your Express + Vite app needs a build-time variable, hardcode the default in your frontend code: import.meta.env.VITE_API_URL || '/api'
8. Wallet insufficient balance for pod tier
The deploy gate requires a 3-month hosting reserve in your wallet. If you are on a higher pod tier but have few tokens, deployment is blocked. Either buy more tokens or lower the pod tier. See the gas pricing table.