← Back to Skills

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.md
Download .md

Coming soon via CLI:

tawa chaac install tawa-user-access

Details

Format
Rule
Category
configure
Version
1.0.47353
Tokens
~1,371
Updated
2026-06-24
platformusersorgmembersbio-idcross-orguser-access