Service Dependencies & Scopes

Scopes control how services access other services' databases and APIs on the Tawa platform. Every cross-service connection — whether a database read or an API call — requires a scope grant before the builder will wire it up.

Overview

There are two kinds of cross-service access on the platform, each managed through a different grant system:

Access typeGrant systemWhat's sharedDeclared in
Database accessKoko scope grantsConnection strings (MongoDB, Redis, Neo4j)spec.scopes (owner) + CLI request (consumer)
API accessBio-ID scope grantsAuthenticated service-to-service API callsspec.dependencies (consumer)
Key principle: No unauthenticated cross-service connections. Every dependency — database or API — goes through an approval gate before the builder injects credentials.

Database access (Koko grants)

Services can share database access with other services through Koko's scope system. The owner declares what it shares, a consumer requests access, and an org admin approves.

How it works

  1. The owner service declares shareable resources in its catalog-info.yaml under spec.scopes
  2. A consumer service requests access via the CLI
  3. The owner's org admin approves or denies the request
  4. On the consumer's next deploy, the builder injects a connection string as an environment variable

Declaring available scopes (owner)

spec:
  scopes:
    - resource: mongodb
      database: shared-data
      allowedConsumers:
        - service: my-consumer
          access: readOnly

The builder registers these scope declarations in Koko on every deploy. Only services listed in allowedConsumers can request access — requests from unlisted services are rejected automatically.

Requesting access (consumer)

tawa scopes request --from owner-service --resource mongodb --access readOnly

Access levels

LevelDescriptionDetails
readWriteFull read and write accessFull CRUD on MongoDB, all Redis commands, full Neo4j access
readOnlyRead-only accessMongoDB read preference enforced, Redis GET only, Neo4j read-only transactions

Resource types

TypeEnv var patternUse case
mongodb{OWNER_SERVICE}_MONGODB_URIShared document stores, cross-service queries
redis{OWNER_SERVICE}_REDIS_URLShared caches, event queues, pub/sub
neo4j{OWNER_SERVICE}_NEO4J_URIShared knowledge graphs, relationship queries

The owner service name is uppercased with hyphens replaced by underscores. For example, if the owner is shared-data-svc and the resource is mongodb:

SHARED_DATA_SVC_MONGODB_URI=mongodb://host:27017/shared-data
No collision with own databases. Your service's own MONGODB_URI is unaffected. Scoped variables always include the owner service name as a prefix.

API access (Bio-ID scope grants)

Service-to-service API calls use the unified spec.dependencies field in your catalog-info.yaml. Every dependency requires a Bio-ID scope grant — there are no unauthenticated pod-to-pod connections.

Declaring dependencies (consumer)

The consuming service declares which services it needs and what scopes it requires:

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

What the builder does

  1. Reads your spec.dependencies declarations
  2. Creates a Bio-ID scope grant request for each dependency
  3. Same-org dependencies are auto-approved. Cross-org dependencies require the target service owner to approve.
  4. On approval, Bio-ID adds the requested scopes to your service's OAuth client
  5. The builder injects credentials based on the transport mode
No manual setup needed. The builder handles OAuth client creation, scope grant requests, and credential injection automatically. You declare dependencies in YAML and the platform wires everything up.

Transport modes

The transport field controls how your service reaches the target service:

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)Consumer calls the Janus URL
Best forHigh-volume internal calls (relay, wallet)Cross-org calls, usage-tracked APIs

Direct transport

Direct transport sends requests over the K8s internal network. No gas is charged, but every request still carries an OAuth Bearer token that the target service validates via Bio-ID introspection.

spec:
  dependencies:
    - service: relay
      scopes: [relay:send]
      transport: direct

The builder injects RELAY_URL pointing to the K8s DNS address. Your service authenticates with its auto-provisioned OAuth client credentials:

// The builder injects RELAY_URL and BIO_CLIENT_ID/BIO_CLIENT_SECRET
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()

const res = await fetch(process.env.RELAY_URL + '/api/relay/send', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ /* ... */ }),
})

Gateway transport

Gateway transport routes requests through Janus. Janus validates the Bearer token and meters gas on successful responses.

spec:
  dependencies:
    - service: raterspot
      scopes: [raterspot:rate]
      transport: gateway

No service URL is injected — the consumer calls the Janus gateway URL directly. The OAuth token carries the granted scopes.

Auth flow

Both transport modes use the same authentication model:

Consumer                  Builder / Bio-ID              Target Service
   │                           │                              │
   │  1. Declare dependency    │                              │
   │  ─────────────────────▶   │                              │
   │                           │  2. Create scope grant       │
   │                           │     request in Bio-ID        │
   │                           │                              │
   │                           │  3. Same-org? Auto-approve   │
   │                           │     Cross-org? Owner decides │
   │                           │                              │
   │  4. Scopes added to       │                              │
   │     consumer's OAuth      │                              │
   │     client                │                              │
   │                           │                              │
   │  5. Get token via         │                              │
   │     client_credentials ──▶│                              │
   │                           │                              │
   │  6. Call target with   ───┼──────────────────────────────▶
   │     Bearer token          │         scope check          │
   │                           │     requireScope('x:y')      │

The target service's internalAuth middleware handles both transport paths:

  • Gateway (Janus): Janus-injected headers carry the validated identity (trusted platform path)
  • Direct: Bearer token is introspected against Bio-ID, scopes are checked

The target service uses requireScope('relay:send') (or equivalent) to enforce that the caller has the right scopes. No changes are needed on the target service to support the unified model.

Approval workflow

Both database and API scope requests go through a state machine:

pending  ──→  approved  ──→  revoked
   │
   └──→  denied
TransitionWhoEffect
pending → approvedOwner org admin (or auto for same-org API deps)Credentials injected on consumer's next deploy
pending → deniedOwner org adminConsumer can re-request with a different note
approved → revokedOwner org adminCredentials removed on consumer's next deploy
Same-org auto-approval: API dependencies (spec.dependencies) between services owned by the same org are auto-approved by Bio-ID. Cross-org dependencies require manual approval from the target service's org admin.
Revocation is not instant. When a grant is revoked, the environment variable is removed on the consumer's next deploy. Until then, the existing credentials continue to work.

Environment variables

Database grants

When a database scope grant is approved and the consumer redeploys, the builder injects connection strings with the owner service name as a prefix:

Resource typeEnv var pattern
mongodb{OWNER_SERVICE}_MONGODB_URI
redis{OWNER_SERVICE}_REDIS_URL
neo4j{OWNER_SERVICE}_NEO4J_URI

API dependencies

When an API scope grant is approved and the consumer redeploys, the builder injects the service URL (for direct transport):

TransportEnv varExample value
direct{SERVICE}_URLhttp://relay.relay-prod.svc.cluster.local:3000
gateway(none — use Janus URL)Consumer calls api.tawa.insureco.io

OAuth credentials (BIO_CLIENT_ID and BIO_CLIENT_SECRET) are always injected when your service has any dependencies or SSO auth. Use them to obtain Bearer tokens via the OAuth client_credentials flow.

CLI commands

Database scopes

# Request access to another service's database
tawa scopes request --from owner-service --resource mongodb --access readOnly

# List all scope grants (database + API)
tawa scopes list

# Approve a pending request (owner org admin)
tawa scopes approve <grantId>

# Deny a pending request
tawa scopes deny <grantId>

# Revoke a previously approved grant
tawa scopes revoke <grantId>

API dependencies

API scope grants are managed automatically through spec.dependencies in your catalog-info.yaml. When you deploy, the builder creates grant requests for any new dependencies. You can view their status:

# View all dependency grants for your service
tawa scopes list --type api

# View pending cross-org approvals (as target service owner)
tawa scopes list --status pending

Common patterns

Sending email via InsureRelay (direct transport)

A common pattern for services that need to send transactional emails. Use transport: direct to avoid gas charges on high-volume internal sends:

# Consumer: my-api/catalog-info.yaml
spec:
  dependencies:
    - service: relay
      scopes: [relay:send]
      transport: direct

The builder injects RELAY_URL, and your service authenticates using its OAuth client credentials with the relay:send scope.

Calling a third-party API through the gateway

Use transport: gateway for cross-org or metered API calls:

# Consumer: my-api/catalog-info.yaml
spec:
  dependencies:
    - service: raterspot
      scopes: [raterspot:rate]
      transport: gateway

Janus validates the Bearer token and charges gas on each successful call.

Shared reporting database

One service owns a MongoDB database that aggregates data for reporting. Multiple consumer services read from it:

# Owner: reporting-svc/catalog-info.yaml
spec:
  databases:
    - type: mongodb
      name: reporting
  scopes:
    - resource: mongodb
      database: reporting
      allowedConsumers:
        - service: dashboard-ui
          access: readOnly
        - service: export-worker
          access: readOnly

Event queue via Redis

One service writes events to Redis, and worker services consume them:

# Owner: event-bus/catalog-info.yaml
spec:
  databases:
    - type: redis
  scopes:
    - resource: redis
      allowedConsumers:
        - service: notification-worker
          access: readWrite
        - service: analytics-worker
          access: readOnly

Mixed dependencies

A service that reads from a shared database and calls an API:

# Consumer: dashboard-ui/catalog-info.yaml
spec:
  # API dependency — auto-approved if same org
  dependencies:
    - service: relay
      scopes: [relay:send]
      transport: direct

  # Database access — requires owner approval via CLI
  # (request via: tawa scopes request --from reporting-svc --resource mongodb --access readOnly)

Legacy behavior

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

Old fieldMapped toBehavior
internalDependenciesdependencies with transport: directWarn but allow (no scope grant check during migration)
externalDependenciesdependencies with transport: gatewayExisting Bio-ID grants honored

Migrate to spec.dependencies at your earliest convenience. The legacy fields will be removed in a future release.

Related