Querying Org Members and User Access

The Short Version

Three patterns — pick the one that fits your use case:

// 1. Same-org users (forward the request token — no infra needed)
const res = await fetch(
  `${process.env.BIO_ID_URL}/api/orgs/${req.user.orgSlug}/members?modules=collections`,
  { headers: { Authorization: req.headers.authorization as string } }
)

// 2. Cross-org or background jobs (declare spec.userAccess in catalog, then deploy)
import { BioUsers } from '@insureco/bio/users'
const users = BioUsers.fromEnv()
const members = await users.getOrgMembers(orgSlug, { modules: ['collections'] })

// 3. "My records" — zero infra needed
const mine = await db.accounts.find({ 'assignedTo.bioId': req.user.bioId })

Pattern 1 — Same-Org (User Token Pass-Through)

Use when: a user is logged in and you want to list users in their own org.

No catalog changes. No new env vars. Forward the user's Bearer token.

app.get('/api/accounts/assignable-users', auth, async (req, res) => {
  const res = await fetch(
    `${process.env.BIO_ID_URL}/api/orgs/${req.user.orgSlug}/members?modules=collections`,
    { headers: { Authorization: req.headers.authorization as string } }
  )
  const { data } = await res.json()
  return reply.json({ users: data.members })
})

Response shape:

{
  "members": [
    {
      "bioId": "usr_abc123",
      "email": "[email protected]",
      "name": "Jane Doe",
      "jobTitle": "Agent",
      "enabled_modules": ["collections", "claims"]
    }
  ],
  "total": 42
}

Available filters: ?modules=X,Y — only users with those modules enabled. ?roles=admin — filter by role. ?limit=50&page=1 — pagination.

Pattern 2 — Cross-Org or Background Jobs

Use when: a background job, cron trigger, or service-to-service call needs to look up users in another org.

Step 1: Declare in catalog-info.yaml

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  annotations:
    insureco.io/catalog-version: "0.5.0"   # required
spec:
  userAccess:
    scope: cross-org        # or 'org' for same-org only
    filter:
      modules: [collections]   # optional — scopes what your service can see

Step 2: Deploy

tawa deploy --prod

The builder injects:

  • BIO_USERS_URLhttps://bio.tawa.pro/api/v2/users
  • BIO_SERVICE_KEY — a sak_xxx service API key bound to your service

Step 3: Use the SDK

import { BioUsers } from '@insureco/bio/users'

const users = BioUsers.fromEnv()  // reads BIO_USERS_URL + BIO_SERVICE_KEY

// List members of a specific org
const members = await users.getOrgMembers('acme-corp', {
  modules: ['collections'],
  limit: 100,
})

// Search across all orgs your service has access to
const agents = await users.listUsers({
  orgSlug: 'acme-corp',
  roles: ['agent'],
  modules: ['collections'],
})

Cross-Org Access Approval

When scope: cross-org, the org you're querying must approve your service's access request. This appears in their Tawa Console → Permissions → User Access (Incoming tab). Until approved, cross-org queries return 403.

Your service's outgoing requests are visible in Console → Permissions → User Access → Outgoing.

NOTE: Same-org access (scope: org) is always approved automatically — no UI action needed.

Pattern 3 — "My Records"

Use when: users need to see only records assigned to them (not a full user list).

Store bioId when assigning a record. Filter by req.user.bioId at query time. No user list query needed at all.

// When assigning
await db.accounts.updateOne(
  { _id: accountId },
  { $set: { assignedTo: { bioId: req.user.bioId, name: req.user.name } } }
)

// When fetching "my accounts"
const mine = await db.accounts.find({ 'assignedTo.bioId': req.user.bioId })

TIP: bioId is stable forever — never changes even if the user's email or name changes. Safe to store as a foreign key.

Decision Table

ScenarioPattern
List users in the logged-in user's orgPattern 1 (token pass-through)
Nightly job assigns records to org membersPattern 2 (service key)
Webhook from another org's servicePattern 2 (cross-org)
Show a user their own recordsPattern 3 (bioId filter)
Real-time user search for assignment UIPattern 1 or 2 depending on who's calling

Env Vars Reference

VariableAlways injected?When injected
BIO_ID_URLYesAlways — core platform var
BIO_USERS_URLNoOnly when spec.userAccess is declared
BIO_SERVICE_KEYNoOnly when spec.userAccess is declared

IMPORTANT: Never construct the Bio-ID URL manually. Always use BIO_ID_URL and BIO_USERS_URL from env.

Common Mistakes

// ❌ WRONG: hardcoded URL
const res = await fetch('https://bio.tawa.pro/api/v2/users?...')

// ✅ CORRECT
const res = await fetch(`${process.env.BIO_USERS_URL}?orgSlug=${orgSlug}`)

// ❌ WRONG: using BIO_USERS_URL without declaring spec.userAccess
// BIO_USERS_URL won't be injected — runtime error

// ❌ WRONG: querying cross-org without declaring scope: cross-org
// Returns 403 — org hasn't approved your service

// ❌ WRONG: storing email as a user foreign key
{ assignedTo: { email: user.email } }  // email can change

// ✅ CORRECT: store bioId (stable forever)
{ assignedTo: { bioId: user.bioId, name: user.name } }

Last updated: July 15, 2026