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 })
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.
Use when: a background job, cron trigger, or service-to-service call needs to look up users in another org.
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
tawa deploy --prod
The builder injects:
BIO_USERS_URL — https://bio.tawa.pro/api/v2/usersBIO_SERVICE_KEY — a sak_xxx service API key bound to your serviceimport { 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'],
})
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.
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:
bioIdis stable forever — never changes even if the user's email or name changes. Safe to store as a foreign key.
| Scenario | Pattern |
|---|---|
| List users in the logged-in user's org | Pattern 1 (token pass-through) |
| Nightly job assigns records to org members | Pattern 2 (service key) |
| Webhook from another org's service | Pattern 2 (cross-org) |
| Show a user their own records | Pattern 3 (bioId filter) |
| Real-time user search for assignment UI | Pattern 1 or 2 depending on who's calling |
| Variable | Always injected? | When injected |
|---|---|---|
BIO_ID_URL | Yes | Always — core platform var |
BIO_USERS_URL | No | Only when spec.userAccess is declared |
BIO_SERVICE_KEY | No | Only when spec.userAccess is declared |
IMPORTANT: Never construct the Bio-ID URL manually. Always use
BIO_ID_URLandBIO_USERS_URLfrom env.
// ❌ 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