User Model

Understanding the relationship between Users, Agents, and Tenants — ZapTicket's identity architecture.

Overview

ZapTicket separates identity (who you are) from workspace membership (where you work). This allows one person to be part of multiple workspaces while keeping data isolated.

Three core entities make up the identity model:

  • User — A person who authenticates
  • Agent — A join between a User and a Tenant (workspace membership)
  • Tenant — A workspace (isolated data environment)

Entity Relationships

Entity Diagram
┌────────────────┐         ┌────────────────┐         ┌────────────────┐
│     User       │         │     Agent      │         │    Tenant      │
├────────────────┤         ├────────────────┤         ├────────────────┤
│ id             │───┐     │ id             │     ┌───│ id             │
│ email          │   │     │ userId     ────│─────│   │ name           │
│ fullName       │   └────▶│ tenantId   ────│─────┘   │ slug           │
│ passwordHash   │         │ role           │         │ siteKey        │
│ emailVerified  │         │ status         │         │ hmacSecret     │
│ createdAt      │         │ lastSeenAt     │         │ createdAt      │
└────────────────┘         │ createdAt      │         └────────────────┘
                           └────────────────┘
                           
User 1 ──── * Agent * ──── 1 Tenant

One user can have many agent records (one per workspace).
One tenant can have many agent records (one per team member).

User

A User represents a person who can authenticate with ZapTicket. Users have an email/password combination and a global identity that exists independently of any workspace.

User Entity
{
  "id": "usr_abc123",
  "email": "[email protected]",
  "fullName": "Jane Smith",
  "emailVerified": true,
  "createdAt": "2024-01-01T09:00:00Z"
}

Key points:

  • A user can exist without belonging to any workspace (freshly registered)
  • Email is globally unique across all users
  • The user token grants no access to workspace data — only workspace listing and selection
  • User profile (name, email) is shared across all workspaces they belong to

Agent

An Agent is the join entity between a User and a Tenant. It represents "this person is a member of this workspace with this role." Think of it as a workspace membership record.

Agent Entity
{
  "id": "agt_xyz789",
  "userId": "usr_abc123",
  "tenantId": "ws_def456",
  "role": "admin",
  "status": "active",
  "lastSeenAt": "2024-01-15T16:45:00Z",
  "createdAt": "2024-01-05T11:00:00Z"
}

Key points:

  • Same user can have different roles in different workspaces (admin in one, agent in another)
  • Agent ID is what appears in conversations, tickets, and assignments
  • Pending agents (invited but not yet accepted) have userId: null
  • The workspace token contains the agent's ID and role in its JWT claims
💡When the system says "assigned to agent agt_xyz789", it means assigned to the specific workspace membership — not the global user. This matters because the same person could be agents in multiple workspaces.

Tenant

A Tenant is a workspace — the top-level isolation boundary. All data (conversations, tickets, messages, settings) belongs to exactly one tenant.

Tenant Entity
{
  "id": "ws_def456",
  "name": "Acme Inc",
  "slug": "acme-inc",
  "siteKey": "zt_pub_abc123",
  "createdAt": "2024-01-01T09:00:00Z"
}

Key points:

  • Slug is globally unique and used in URLs
  • Each tenant has its own site key and HMAC secret
  • Data never crosses tenant boundaries (enforced by global query filter)
  • Ticket references (ZT-1001) are unique within a tenant, not globally

How They Relate in Practice

Example Scenario
Jane (User: usr_abc123)
  ├── Agent: agt_001 in "Acme Inc" (role: admin)
  │     → Can manage settings, invite team, handle chats
  │
  └── Agent: agt_002 in "Side Project" (role: agent)
        → Can only handle chats and tickets

When Jane selects "Acme Inc":
  → JWT contains { tenantId: "ws_acme", agentId: "agt_001", role: "admin" }
  → All API calls scoped to Acme Inc data
  → She sees Acme's conversations, not Side Project's

When Jane selects "Side Project":
  → JWT contains { tenantId: "ws_side", agentId: "agt_002", role: "agent" }
  → All API calls scoped to Side Project data
  → Settings pages hidden (agent role, not admin)

Visitors

Visitors are not Users. They don't have accounts or passwords. A visitor is identified by their name and email (if provided) and exists only within the context of a conversation. Visitor data is stored on the conversation entity, not as a separate user record.

⚠️Don't confuse Users (who log in to the dashboard) with Visitors (who chat through the widget). They are entirely separate concepts with different authentication mechanisms.