Passport — Enriched Identity, Branding & Permissions
A Passport is a self-contained identity object that travels with a user across every Tawa service. It carries identity, org branding, and permissions — all…
Ruleprovision
About
A Passport is a self-contained identity object that travels with a user across every Tawa service. It carries identity, org branding, and permissions — all bundled at token mint time by Bio-ID. Services never make a second call to get this data.
Skill Content
This is the raw markdown that gets installed as a Claude Code rule.
# Passport — Enriched Identity, Branding & Permissions
## What It Is
A Passport is a self-contained identity object that travels with a user across every Tawa service. It carries identity, org branding, and permissions — all bundled at token mint time by Bio-ID. Services never make a second call to get this data.
One login, everywhere. One object, everything you need.
> Canonical spec: `tawa-platform/architecture/passport-branding-auth` in the Bible.
---
## The Passport Object
```typescript
interface Passport {
// Identity
bioId: string // unique user ID (immutable)
email: string
firstName: string
lastName: string
displayName: string
avatar?: string
userType: string // 'user' | 'service' | 'admin'
roles: string[] // roles in primary org
// Primary org
orgSlug: string
orgId: string // ORG-xxx format
orgName: string
// All org memberships (multi-org users)
orgs: {
orgSlug: string
orgId: string
orgName: string
roles: string[]
modules: string[]
}[]
// Aggregated modules across all orgs
modules: string[]
// Branding — loaded from vault at mint time (when passport.includeBranding: true)
branding: {
displayName: string // org display name for UI chrome
tagline?: string
logoUrl?: string // primary logo (SVG or PNG)
logoMarkUrl?: string // icon/mark for small surfaces
primaryColor: string // hex — defaults to '#0F172A'
secondaryColor?: string // hex
websiteUrl?: string
verified: boolean // platform verified this entity
whiteLabelApproved: boolean // approved for custom domain + hideEcoBranding
}
// Permissions — resolved from Koko at mint time
permissions: {
scopes: string[] // e.g. ['raterspot:rate', 'docman:read']
}
// Vault link
iecHash?: string // entity's chain address (if vault entity exists)
// Session metadata
issuedAt: string
connectedServices: string[]
}
```
---
## Using the Passport in Your Service
### Reading Identity + Branding (Most Common)
```typescript
// req.user IS the Passport — already populated by Janus or your middleware
const { orgSlug, branding, permissions } = req.user
// Brand the page
document.title = branding.displayName
headerLogo.src = branding.logoUrl
root.style.setProperty('--primary', branding.primaryColor)
// Check permission
if (permissions.scopes.includes('raterspot:rate')) {
showRatingPanel()
}
// No vault lookup. No Koko call. It's already in the token.
```
### React — Via Passport Socket
```typescript
import { useEffect, useState } from 'react'
const BIO_ID_URL = process.env.NEXT_PUBLIC_BIO_ID_URL || 'https://bio.insureco.io'
export function usePassport(token: string | null) {
const [passport, setPassport] = useState<Passport | null>(null)
useEffect(() => {
if (!token) return
const ws = new WebSocket(`${BIO_ID_URL.replace(/^http/, 'ws')}/passport?token=${token}`)
ws.onmessage = (e) => {
const { type, passport } = JSON.parse(e.data)
if (type === 'identity' || type === 'passport_updated') setPassport(passport)
if (type === 'revoked') setPassport(null)
}
ws.onclose = () => setPassport(null)
return () => ws.close()
}, [token])
return passport
}
```
### Node.js / CLI
```typescript
import WebSocket from 'ws'
const BIO_ID_URL = process.env.BIO_ID_URL || 'https://bio.insureco.io'
export function connectPassport(token: string, onIdentity: (passport: Passport) => void) {
const ws = new WebSocket(`${BIO_ID_URL.replace(/^http/, 'ws')}/passport?token=${token}`)
ws.on('message', (raw) => {
const { type, passport } = JSON.parse(raw.toString())
if (type === 'identity' || type === 'passport_updated') onIdentity(passport)
if (type === 'revoked') process.exit(0)
})
return () => ws.close()
}
```
---
## The Socket
```
wss://bio.insureco.io/passport?token=<access_token>
```
Advertised in Bio-ID's OIDC configuration as `passport_endpoint`.
### Events
| Event type | When | Payload |
|------------|------|---------|
| `identity` | On connect (if session valid) | `{ type, passport }` |
| `passport_updated` | Role/org/branding change | `{ type, passport }` |
| `revoked` | Logout or token invalidated | `{ type }` |
### Error Codes
| Code | Reason |
|------|--------|
| `4001` | Missing token — no `?token=` in URL |
| `4003` | Invalid token — expired or tampered |
| `4004` | User not found or suspended |
---
## Three Auth Modes
All three modes produce the same Passport object. The mode determines the user experience.
| Mode | Bio-ID visible? | User redirected? | Dev builds UI? |
|------|----------------|-----------------|----------------|
| **Branded** | Yes — "Sign in with Bio-ID" | Yes (bio.tawa.pro) | No |
| **White-Label** | No — org's domain + branding | Yes (custom domain) | No (CSS only) |
| **Headless** | No — completely invisible | No — server-to-server | Yes — fully custom |
### Branded (default)
```yaml
spec:
auth:
mode: sso
```
### White-Label
```yaml
spec:
auth:
mode: sso
passport:
includeBranding: true
```
Org's vault branding auto-applies to the consent screen. Custom domain + hideEcoBranding requires `whiteLabelApproved: true` on the vault entity.
### Headless
```yaml
spec:
auth:
mode: sso
passport:
includeBranding: true
headless: true
allowedOrigins:
- https://yourapp.com
```
```typescript
const bio = BioAuth.fromEnv()
// Signup — server-to-server, no redirect
const tokens = await bio.embed.signup({
email, password, firstName,
orgSlug: 'acme-agency', // optional — omit to auto-create org
})
// Login
const tokens = await bio.embed.login({ email, password })
// Magic link (you send the email yourself)
const { magicToken } = await bio.embed.createMagicLink({ email })
// Verify magic token
const tokens = await bio.embed.verify({ token: magicToken })
// Refresh
const tokens = await bio.embed.refresh({ refreshToken })
```
---
## Onboarding Flows
### Self-Signup (no invite)
```
Any mode → user signs up → Bio-ID auto-creates personal org (slug from name)
→ Passport minted with new org → user lands in service
```
### Invite
```
Admin: POST /api/v2/orgs/:orgSlug/invites { email, role, modules }
→ Bio-ID stores invite, sends email (or returns inviteUrl for headless)
→ User clicks link → signup (if new) or login (if existing)
→ Attached to inviting org with specified role
→ Passport minted with inviting org as primary
```
Headless invite acceptance:
```typescript
const tokens = await bio.embed.signup({
email, password, firstName,
inviteToken: 'TOKEN', // resolves to org + role
})
```
### Service-Initiated (with orgSlug)
```typescript
// Create user directly in an org (requires org:manage scope)
const tokens = await bio.embed.signup({
email: '[email protected]',
password: tempPassword,
firstName: 'New',
orgSlug: 'acme-agency',
role: 'agent',
})
```
---
## Vault Branding
Branding lives on vault entities at `profile.branding`. The Passport reads it at mint time.
### Two Tiers
| Tier | Who | What they can set |
|------|-----|-------------------|
| **Claimed** | Any entity after claim flow | displayName, tagline, logoUrl, logoMarkUrl, colors, website, phone, email |
| **White-Label Approved** | Platform approval | customDomain, hideEcoBranding, emailFromName, customCss |
### Vault API
```
GET /v1/branding/:orgSlug → 0 gas, public
PATCH /v1/entities/:iecHash/branding → 2 gas, auth required
```
### SDK
```typescript
import { VaultClient } from '@insureco/bio-vault'
const vault = VaultClient.fromEnv()
const branding = await vault.getBranding('acme-agency')
```
---
## How It Fits with OAuth
| Flow | Use when |
|------|----------|
| Full-page redirect | First login, consent screens |
| Popup | Staying on page during auth |
| Headless embed API | Full control, Bio-ID invisible |
| **Passport socket** | Already authenticated — propagate identity silently |
After any OAuth flow completes, Bio-ID publishes the Passport on the socket. Any connected service receives the identity immediately.
---
## Key Use Cases
- **Branding** — `req.user.branding.logoUrl` renders the org's logo instantly
- **Onboarding** — passport arrives with name, org, modules → no "what's your company?" form
- **Badge widget** — floating identity indicator on any page
- **CLI** — connect on startup, get identity silently
- **Village traversal** — user moves between services, identity follows
- **Permission gating** — `req.user.permissions.scopes` controls feature access
---
## Gotchas
- Token must be a valid Bio-ID access token (RS256 in prod)
- Socket closes on invalid token — don't retry without a fresh token
- Branding in token is cached for up to 1 hour — logo changes propagate on next refresh
- If `passport.includeBranding` is not declared (or `false`) in catalog-info.yaml, Passport carries identity only (no branding)
- Headless mode: always call Bio-ID server-to-server, never from the browser
- `bio.embed.signup({ orgSlug })` requires `org:manage` scope — auto-granted when deployed by that org
Install
Copy the skill content and save it to:
~/.claude/rules/tawa-passport.mdComing soon via CLI:
tawa chaac install tawa-passportDetails
- Format
- Rule
- Category
- provision
- Version
- 1.0.41477
- Tokens
- ~2,313
- Updated
- 2026-06-24
platformpassportidentityauthbrandingpermissions