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-org

This 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

FieldTypeRequiredDescription
namestringYesService name. Must be lowercase alphanumeric + dashes. This becomes your hostname: <name>.tawa.insureco.io
descriptionstringYesBrief description of what this service does
tagsstring[]NoTags 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.

AnnotationValuesDefaultWhat it does
insureco.io/frameworkexpress | nextjs | hono | fastify | static | workerunknownDetermines Dockerfile template, build/start commands, and output directory
insureco.io/node-versionAny Node.js version string20Node.js base image tag in Dockerfile
insureco.io/pod-tiernano | small | medium | large | xlargenanoRAM allocation and hosting gas rate. See Gas & Wallets
insureco.io/portInteger (as string)3000 (or 80 for static)Container port — EXPOSE in Dockerfile, PORT env var
insureco.io/health-endpointURL path/health or /api/healthK8s liveness/readiness probe path. Must return 200.
insureco.io/build-commandShell commandnpm run buildCustom build command in Dockerfile RUN step
insureco.io/start-commandShell commandFramework defaultCustom CMD in Dockerfile
insureco.io/output-dirDirectory pathdist or .nextBuild output directory copied to production stage
insureco.io/env-varsComma-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-pathsComma-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/homepageURL(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.
Framework defaults: If you set 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)

FieldTypeRequiredDescription
typestringYesAlways service
lifecyclestringYesproduction | experimental | deprecated
ownerstringYesOrganization slug (e.g., insureco). Must match your Bio-id org slug. Used to derive wallet ID: wallet-<owner>
Critical: The 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

TypeEnv var injectedConnection string format
mongodbMONGODB_URImongodb://<host>:27017/<name>-<environment>
redisREDIS_URLredis://<host>:6379/0
neo4jNEO4J_URIbolt://<host>:7687

Example

spec:
  databases:
    - type: mongodb
    - type: redis

This 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-db
Only 3 types are supported. Using any other value (e.g., postgres, mysql) will fail during tawa preflight validation.

What the builder creates

For each database entry, the builder:

  1. Creates a K8s Secret named <service>-db-<type> (e.g., my-api-db-mongodb)
  2. Injects the env var via Helm --set
  3. 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

FieldTypeRequiredDescription
pathstringYesURL path (e.g., /api/users). Supports :param syntax.
methodsstring[]YesGET, POST, PUT, DELETE, PATCH
authstringNorequired | optional | none (default: none)
descriptionstringNoHuman-readable description of the endpoint
scopesstring[]NoRequired scopes for this route (future enforcement)
gasintegerNoGas 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 charged

How routes work with Janus

When a service calls your API through the gateway (api.tawa.insureco.io), Janus:

  1. Looks up the service in Koko by path
  2. Validates the HTTP method is allowed
  3. Checks auth if the route requires it
  4. Checks the caller's wallet has enough gas
  5. Proxies the request to your pod
  6. 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:

  1. Route-level: The gas value in your catalog-info.yaml (recommended)
  2. Platform table: Hardcoded fallback for legacy services not yet migrated
  3. 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.

No routes? If you omit 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).
Gas is only charged on 2xx responses. If your service returns a 4xx or 5xx error, no gas is consumed. This protects callers from paying for failed requests.

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

FieldTypeRequiredDescription
servicestringYesTarget service name (as registered in Koko)
scopesstring[]YesOAuth scopes to request (e.g. [relay:send])
transportstringYesdirect (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 metered

Transport modes

directgateway
Network pathK8s internal DNS (pod-to-pod)Through Janus gateway
Gas meteredNoYes
Scope grant requiredYesYes
Auth on each requestBearer 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:

  1. Creates a Bio-ID scope grant request for the declared scopes
  2. Same-org dependencies are auto-approved; cross-org requires owner approval
  3. On approval, adds scopes to your service's OAuth client
  4. For direct transport: resolves K8s DNS URL via Koko, injects {SERVICE}_URL
  5. For gateway transport: consumer calls the Janus URL with a Bearer token
DependencyTransportEnv varExample value (production)
relaydirectRELAY_URLhttp://relay.relay-prod.svc.cluster.local:3000
raterspotgateway(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}` },
  // ...
})
Blocked if not approved. If a scope grant is denied or revoked, the builder blocks the deploy. Pending grants emit a warning during the migration window, then block.

Legacy fields

The old internalDependencies and externalDependencies fields are still supported during the migration window:

  • internalDependencies → mapped to dependencies with transport: direct (no scope check during migration)
  • externalDependencies → mapped to dependencies with transport: 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
ModeDescriptionOAuth client?
ssoUsers authenticate via Bio-id. Best for web apps with user login.Yes — auto-provisioned
service-onlyService-to-service auth only. No user-facing login.Yes — auto-provisioned
noneNo 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>-oauth with BIO_CLIENT_ID and BIO_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

FieldTypeRequiredDescription
keystringYesEnvironment variable name. Must be a valid env var (letters, digits, underscores, starting with a letter or underscore).
secretbooleanNoIf true, values are encrypted at rest (AES-256-GCM) and never returned by the API. Default: false
requiredbooleanNoIf true, the deploy blocks if this key has no value set and no default. Default: false
defaultstringNoFallback 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_NAME

How it works

  1. On deploy: The builder reads your declarations, validates that all required keys have a value (or default), and caches them on the service record.
  2. Preflight check: If any required key is missing, the deploy fails before the Docker build, saving time and compute.
  3. Defaults: Keys with a default value 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_xxx

When 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.

First deploy: Config declarations are synced to the builder during deploy. Before your first deploy, use tawa config set to set values directly. After the first deploy, tawa config push becomes available.
Preflight validation: Run 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: sso

What this creates

ResourceValue
Pod tiermedium — 1 GB RAM, 23 tokens/hour
Deploy gate reserve23 × 730 = 16,790 tokens required
K8s secretsplatform-api-db-mongodb, platform-api-db-redis, platform-api-oauth
Env varsMONGODB_URI, REDIS_URL, IEC_WALLET_URL, BIO_CLIENT_ID, BIO_CLIENT_SECRET
Routes registered4 routes in Koko with gas pricing (0–5 tokens/call), accessible via Janus gateway
Production URLplatform-api.tawa.insureco.io

Environment Variables

The builder injects environment variables in this order of precedence (later values override earlier ones):

  1. Catalog defaults: Values from spec.config[].default (lowest priority)
  2. Database vars: MONGODB_URI, REDIS_URL, NEO4J_URI
  3. Service discovery vars: <SERVICE>_URL from dependencies (direct transport)
  4. OAuth vars: BIO_CLIENT_ID, BIO_CLIENT_SECRET
  5. Managed config: Values set via tawa config set or tawa config push
  6. Managed secrets: Encrypted values (set via --secret or 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 list

Common Mistakes

1. Missing or wrong framework annotation

# WRONG: missing framework, builder uses "unknown"
metadata:
  annotations: {}
# CORRECT: explicit framework
metadata:
  annotations:
    insureco.io/framework: express

Without 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: mongodb

Unsupported 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 org

The 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/health

K8s 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.