Querying Org Members and User Access
Three patterns — pick the one that fits your use case:
Ruleconfigure
About
Three patterns — pick the one that fits your use case:
Skill Content
This is the raw markdown that gets installed as a Claude Code rule.
# Querying Org Members and User Access
## The Short Version
Three patterns — pick the one that fits your use case:
```typescript
// 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.
```typescript
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:**
```json
{
"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
```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
```bash
tawa deploy --prod
```
The builder injects:
- `BIO_USERS_URL` — `https://bio.tawa.pro/api/v2/users`
- `BIO_SERVICE_KEY` — a `sak_xxx` service API key bound to your service
### Step 3: Use the SDK
```typescript
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.
```typescript
// 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
| 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 |
## Env Vars Reference
| 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_URL` and `BIO_USERS_URL` from env.
## Common Mistakes
```typescript
// ❌ 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 } }
```
Install
Copy the skill content and save it to:
~/.claude/rules/tawa-user-access.mdComing soon via CLI:
tawa chaac install tawa-user-accessDetails
- Format
- Rule
- Category
- configure
- Version
- 1.0.47353
- Tokens
- ~1,371
- Updated
- 2026-06-24
platformusersorgmembersbio-idcross-orguser-access