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 type | Grant system | What's shared | Declared in |
|---|---|---|---|
| Database access | Koko scope grants | Connection strings (MongoDB, Redis, Neo4j) | spec.scopes (owner) + CLI request (consumer) |
| API access | Bio-ID scope grants | Authenticated service-to-service API calls | spec.dependencies (consumer) |
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
- The owner service declares shareable resources in its
catalog-info.yamlunderspec.scopes - A consumer service requests access via the CLI
- The owner's org admin approves or denies the request
- 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: readOnlyThe 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 readOnlyAccess levels
| Level | Description | Details |
|---|---|---|
readWrite | Full read and write access | Full CRUD on MongoDB, all Redis commands, full Neo4j access |
readOnly | Read-only access | MongoDB read preference enforced, Redis GET only, Neo4j read-only transactions |
Resource types
| Type | Env var pattern | Use case |
|---|---|---|
mongodb | {OWNER_SERVICE}_MONGODB_URI | Shared document stores, cross-service queries |
redis | {OWNER_SERVICE}_REDIS_URL | Shared caches, event queues, pub/sub |
neo4j | {OWNER_SERVICE}_NEO4J_URI | Shared 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-dataMONGODB_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 meteredWhat the builder does
- Reads your
spec.dependenciesdeclarations - Creates a Bio-ID scope grant request for each dependency
- Same-org dependencies are auto-approved. Cross-org dependencies require the target service owner to approve.
- On approval, Bio-ID adds the requested scopes to your service's OAuth client
- The builder injects credentials based on the transport mode
Transport modes
The transport field controls how your service reaches the target service:
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) | Consumer calls the Janus URL |
| Best for | High-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: directThe 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: gatewayNo 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| Transition | Who | Effect |
|---|---|---|
| pending → approved | Owner org admin (or auto for same-org API deps) | Credentials injected on consumer's next deploy |
| pending → denied | Owner org admin | Consumer can re-request with a different note |
| approved → revoked | Owner org admin | Credentials removed on consumer's next deploy |
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.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 type | Env 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):
| Transport | Env var | Example value |
|---|---|---|
direct | {SERVICE}_URL | http://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 pendingCommon 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: directThe 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: gatewayJanus 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: readOnlyEvent 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: readOnlyMixed 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 field | Mapped to | Behavior |
|---|---|---|
internalDependencies | dependencies with transport: direct | Warn but allow (no scope grant check during migration) |
externalDependencies | dependencies with transport: gateway | Existing Bio-ID grants honored |
Migrate to spec.dependencies at your earliest convenience. The legacy fields will be removed in a future release.
Related
- Databases — declaring and connecting to your own databases
- catalog-info.yaml Reference — full field reference including dependencies
- InsureRelay — sending email/SMS via the relay:send scope
- OAuth Integration — service OAuth and scope grants
- Environment Variables — how all env vars are injected