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
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ 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.
{
"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.
{
"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
Tenant
A Tenant is a workspace — the top-level isolation boundary. All data (conversations, tickets, messages, settings) belongs to exactly one tenant.
{
"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
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.