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

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)

// 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

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

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 typeWhenPayload
identityOn connect (if session valid){ type, passport }
passport_updatedRole/org/branding change{ type, passport }
revokedLogout or token invalidated{ type }

Error Codes

CodeReason
4001Missing token — no ?token= in URL
4003Invalid token — expired or tampered
4004User not found or suspended

Three Auth Modes

All three modes produce the same Passport object. The mode determines the user experience.

ModeBio-ID visible?User redirected?Dev builds UI?
BrandedYes — "Sign in with Bio-ID"Yes (bio.tawa.pro)No
White-LabelNo — org's domain + brandingYes (custom domain)No (CSS only)
HeadlessNo — completely invisibleNo — server-to-serverYes — fully custom

Branded (default)

spec:
  auth:
    mode: sso

White-Label

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

spec:
  auth:
    mode: sso
    passport:
      includeBranding: true
      headless: true
      allowedOrigins:
        - https://yourapp.com
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:

const tokens = await bio.embed.signup({
  email, password, firstName,
  inviteToken: 'TOKEN',  // resolves to org + role
})

Service-Initiated (with orgSlug)

// 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

TierWhoWhat they can set
ClaimedAny entity after claim flowdisplayName, tagline, logoUrl, logoMarkUrl, colors, website, phone, email
White-Label ApprovedPlatform approvalcustomDomain, hideEcoBranding, emailFromName, customCss

Vault API

GET  /v1/branding/:orgSlug                    → 0 gas, public
PATCH /v1/entities/:iecHash/branding          → 2 gas, auth required

SDK

import { VaultClient } from '@insureco/bio-vault'
const vault = VaultClient.fromEnv()

const branding = await vault.getBranding('acme-agency')

How It Fits with OAuth

FlowUse when
Full-page redirectFirst login, consent screens
PopupStaying on page during auth
Headless embed APIFull control, Bio-ID invisible
Passport socketAlready 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

  • Brandingreq.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 gatingreq.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

Last updated: July 15, 2026