Tendkit

Search documentation

Find a docs page

Get Started

Your directory

Tell Tendkit who wrote: users, customers and agents from your own database.

Tendkit doesn't keep its own list of customers or agents. The directory answers those questions from your database, which is what lets every ticket sit next to the right account.

The interface

NameTypeDescription
identify*(identity) => { customerId, userId?, name?, locale? } | nullCalled for new senders, and for senders not yet linked to a customer. Return null for strangers.
searchCustomers*(query) => { id, name }[]Powers the link-to-customer picker and inbox search.
getCustomers*(ids) => { id, name }[]Names for the customers shown in the inbox.
listAgents*() => { id, name, email? }[]Who can be assigned. The agent id is what your API passes as agentId.

locale is optional. When you return it, it's stored on the contact and passed to acknowledgement, so you can answer in the customer's language.

Example

lib/support/directory.ts
import type { SupportDirectory } from "@/lib/support/service";
import { and, eq, ilike, inArray, sql } from "drizzle-orm";

export const directory: SupportDirectory = {
  async identify({ kind, value }) {
    // value is normalized: lowercase email, or an E.164 phone number.
    const user =
      kind === "email"
        ? await db.query.users.findFirst({ where: sql`lower(${users.email}) = ${value}` })
        : await db.query.users.findFirst({ where: eq(users.phone, value) });
    if (!user) return null;
    return { customerId: user.accountId, userId: user.id, name: user.name, locale: user.locale };
  },

  searchCustomers: (query) =>
    db
      .select({ id: accounts.id, name: accounts.name })
      .from(accounts)
      .where(ilike(accounts.name, `%${query}%`))
      .limit(10),

  getCustomers: (ids) =>
    db.select({ id: accounts.id, name: accounts.name }).from(accounts).where(inArray(accounts.id, ids)),

  listAgents: () =>
    db
      .select({ id: users.id, name: users.name, email: users.email })
      .from(users)
      .where(and(eq(users.role, "admin"), eq(users.active, true))),
};

Unknown senders

Mail from an address identify doesn't know still opens a ticket, marked as an unknown sender. An agent links it to a customer once from the ticket. The link is stored on the contact, so that sender's other unlinked tickets and every future message land on the right account.

Trust

Spoofed senders are never linked

A message that fails DMARC, or fails both SPF and DKIM, isn't matched to an account even when the address is known, gets no automatic reply, and can only thread into a ticket through the reply address token. Account holders are never blocked by a spam mark, since a mark may have come from a spoofed message.