TaifaSupport Docs
Concepts

The chain

Visitor to Session to Customer to Conversation to Ticket. The product thesis, and the five foreign keys that make it true.

Everything else in this documentation is detail. This page is the argument.

Visitor, Session, Customer, Conversation and Ticket are all the same record.

Visitor
visitors.id
One browser, recognised across visits by an opaque token. Anonymous until something identifies it.
Session
visitor_sessions.visitor_id
One continuous visit. Carries referrer, UTM, device, country, duration and page count.
Customer
visitors.customer_id
A known person, created by identify(). Linking one back-fills every earlier session.
Conversation
conversations.visitor_id
A chat thread, carrying the visitor, the customer and the session it started in.
Ticket
tickets.conversation_id
A tracked obligation with a status, a priority and an SLA clock, still pointing at the row above.

Why this is a schema claim

Every support product will tell you it gives an agent context. Most of them mean that a sidebar fetches some related data when a chat opens, and that the join is done at read time by a feature, against whatever the systems happened to record.

Here the links are foreign keys on rows that already exist. That difference shows up in three places where the read-time approach quietly fails:

  1. A visitor who was anonymous for weeks. When identify() finally names them, Visitor.customer_id is set and every prior session is back-filled. Their history did not begin this morning. A read-time join has nothing to join to, because nothing linked those sessions to anyone.
  2. A ticket read a month later. origin_path, visitor_id and customer_id are columns on the ticket, written when it was escalated. The answer to "which page produced this" survives even after the session has been pruned.
  3. A page-level support report. most_support_requests in analytics is only computable because the originating path rode along from the conversation to the ticket. It is not derivable afterwards from anything else.
FromToColumnWritten when
SessionVisitorvisitor_sessions.visitor_idThe session starts
VisitorCustomervisitors.customer_ididentify() succeeds
ConversationVisitor, Customer, Sessionconversations.visitor_id, .customer_id, .session_idThe thread is created
TicketConversationtickets.conversation_idEscalation
TicketVisitor, Customertickets.visitor_id, .customer_idEscalation, carried through

Note that the ticket keeps its own pointers to the visitor and the customer rather than reaching them through the conversation. That is not redundancy for its own sake: a ticket can be filed without a conversation, and a conversation can be deleted under a retention policy while the ticket must survive.

Walking it forward

A real sequence, in the order the rows appear.

A browser arrives

POST /v1/track/session creates a Visitor (or recognises a returning one by its opaque token) and a VisitorSession.

The session captures referrer, referrer_host, utm_source, utm_medium, utm_campaign, landing_path, device_type, browser, os, country_code and language. All of it at the moment of arrival, because most of it is not recoverable later.

They read things

POST /v1/track/pageview writes a PageView and closes the previous one's duration, so duration_seconds and page_view_count are always current rather than computed at the end.

Anything worth a line on a human-readable timeline also writes a VisitorEvent: a knowledge base search, an article read, the chat opening.

They sign in

The portal calls TaifaSupport.identify({ id: "183", ... }).

POST /v1/track/identify creates or finds the Customer for that external_id, sets Visitor.customer_id, and back-fills every prior session. The person's history did not start at this moment; the system only just learned whose it was.

They ask something

A Conversation is created carrying visitor_id, customer_id, session_id and site_id.

The agent's screen already has everything on the live row: the pages, the time, the country, the open tickets. Nothing had to be asked for.

It becomes an obligation

POST /v1/conversations/{id}/escalate creates a Ticket carrying the visitor, the customer and origin_path.

A TicketEvent is written for every change from then on, so the record of who changed what and when is not a reconstruction from application logs.

What this looks like on screen

James MwangiKE
Acme Ltd
Pricing → Enterprise
online 8m 42s, 6 pages
2 open tickets

Not a mock-up. Every value is a column, and the row is one query.

Why the competition does not have this

Not because it is hard, but because their visitor list was never the point. The common set of columns is name, last page, country, browser, entry time, tags and new-or-returning, with two filters and no sorting.

Absent entirely, and present here:

Missing elsewhereColumn here
ReferrerVisitorSession.referrer, .referrer_host
Traffic source and campaign.utm_source, .utm_medium, .utm_campaign
Time on site.duration_seconds, .started_at
Page-view count.page_view_count, Visitor.page_view_count
Full page path with timestampsThe PageView table
Open tickets on the rowtickets.visitor_id

Every one of those is already stored. That is the clearest single reason an Institution picks this, which is why the /visitors screen shows all of it and why any feature that does not serve that row is a lower priority than one that does.

What follows from the thesis

Two rules that come directly out of it:

  • page_views is designed to be pruned first. It is dense, high-volume, and the least valuable row for row. visitor_events is the sparse, human-readable timeline an agent actually reads, and it is kept longer. See pruning.
  • The visitor stream is a privilege. guest, the role for an external collaborator on one ticket, deliberately cannot see any of it. Whole-journey visibility is exactly the thing you do not hand to a vendor.

On this page