Seventy Seven: open-source customer support with a real API

April 24, 2024

|repo-review

by Florian Narr

Seventy Seven: open-source customer support with a real API

Seventy Seven is an open-source customer support platform — a self-hostable Zendesk alternative. You embed the SDK in your app, it creates tickets via a REST endpoint, and your team manages them in a Next.js dashboard backed by Supabase.

Why I starred it

Most "open-source Zendesk alternatives" stop at the UI. They give you an inbox but no story for how your app sends tickets into it. Seventy Seven ships a typed TypeScript SDK alongside the dashboard, so there's a clear path from "user hits submit" in your product to "ticket appears in inbox" — no manual webhook wiring.

What also caught my eye: the repo reaches for the modern SaaS stack in a deliberate way. Trigger.dev for background jobs, Resend for transactional email, pgvector for AI-generated ticket summaries. The pieces fit together, and reading the source shows someone thought through the full lifecycle — creation, assignment, snoozing, closing, notifications.

How it works

The monorepo has two apps (dashboard, website) and several packages: orm, sdk, email, integrations, ui. The interesting work happens in the dashboard app and the SDK.

Ticket creation flow. The API endpoint at apps/dashboard/src/app/api/tickets/route.ts handles POST /api/tickets. It validates the request body with Zod, looks up the team by auth_token, creates the ticket in Postgres via Prisma, then fans out: posts a Slack block message if the Slack integration is configured, and sends an email via Resend to every team member with notification_email_new_ticket enabled.

// apps/dashboard/src/app/api/tickets/route.ts
const foundTeam = await prisma.team.findFirst({
  where: { auth_token: apiToken },
  select: {
    id: true,
    integration_slack: {
      select: {
        slack_access_token: true,
        slack_bot_user_id: true,
        slack_channel_id: true,
      },
    },
    members: {
      select: {
        user: {
          select: {
            full_name: true,
            email: true,
            notification_email_new_ticket: true,
          },
        },
      },
    },
  },
})

The team's auth_token is used as a bearer token — simple and predictable. No OAuth dance for the intake side.

The SDK. packages/sdk/src/index.ts is a ~60-line class. It wraps fetch, stores the token as a private field (#authToken), and exposes one method: createTicket. The type parameter TMeta makes the conditional return type narrow: if you pass metadata, you get it back in the response. Clean TypeScript.

export class SeventySevenClient {
  #authToken: string

  constructor(authToken: string) {
    this.#authToken = authToken
  }

  async createTicket<TMeta= undefined>(ticket: CreateTicketPayload<TMeta>) {
    // ...
    return createdTicket as CreateTicketResponse<TMeta>
  }
}

AI summaries and embeddings. This is the most interesting part. apps/dashboard/src/trigger/generate-ticket-summary.ts defines a Trigger.dev background task (generate-ticket-summary) that fetches all messages in a ticket, formats them as a conversation transcript, and asks gpt-4o-mini to write a 500-character summary. Then it embeds the summary with text-embedding-3-small and stores the vector in Postgres via a raw SQL UPDATE — because Prisma doesn't natively support the pgvector extension type:

await tx.$executeRawUnsafe(
  `UPDATE "tickets" SET summary = $1, summary_embedding = $2::vector WHERE id = $3::uuid`,
  summary,
  embedding,
  ticket.id,
)

The Prisma schema reflects this: summary_embedding Unsupported("vector")?. The pgvector extension is registered in the datasource block with extensions = [pgvector(map: "vector", schema: "extensions")]. So the schema acknowledges the limitation honestly rather than hiding it.

Snooze with scheduled wake-up. The unsnooze-ticket task in Trigger.dev clears snoozed_until, then sends a SnoozeExpired email with the last five messages as thread context. The ticket URL switches based on VERCEL_ENV === 'production', which is a pragmatic shortcut — works, but breaks in staging environments that aren't localhost.

Data model. The Prisma schema is clean. Tickets have short_id (a human-readable identifier generated by shortId()), optional starred_at, snoozed_until, closed_at, and assigned_to_user_id. Tags are a many-to-many join table (ticket_tags_on_tickets). Team members get per-user notification preferences directly on the User model.

Using it

Install the SDK:

npm install @seventy-seven/sdk

Create a ticket from your app:

import { SeventySevenClient } from '@seventy-seven/sdk'

const client = new SeventySevenClient(process.env.SEVENTY_SEVEN_TOKEN)

const ticket = await client.createTicket({
  subject: 'Payment failed on checkout',
  body: 'I was charged but no confirmation email arrived.',
  senderFullName: 'Alex Kim',
  senderEmail: 'alex@example.com',
  meta: { orderId: 'ord_8821', plan: 'pro' },
})

console.log(ticket.id) // UUID
console.log(ticket.meta.orderId) // typed correctly via TMeta

The meta field is z.record(z.string(), z.unknown()) on the server — freeform JSON stored in Postgres. Useful for linking tickets back to orders, users, or sessions without schema changes.

Rough edges

The SDK is a single method. You can create tickets but you can't query them, close them, or reply from client code. The dashboard's tRPC API (apps/dashboard/src/trpc/routers/tickets-router.ts) has all of that functionality — filtering by status, tags, assignees, full-text query — but it's not exposed in the public SDK. If you want to build a self-service support portal on top of this, you're writing custom API routes.

The Slack integration (packages/integrations/src/slack/) exists but the OAuth flow for connecting Slack isn't visible in the public API surface — that's dashboard-only for now.

No tests anywhere in the repo. For a project that handles customer data and transactional email, that's a meaningful gap.

The VERCEL_ENV check in unsnooze-ticket.ts for toggling between localhost and production URLs is the kind of thing that quietly causes issues in preview deployments.

Documentation is thin. The SDK README.md is four lines. The main README describes the stack but not how to self-host or run the database migrations.

Bottom line

If you're building a SaaS and want a simple way to route support tickets without paying for Zendesk, Seventy Seven gives you a solid foundation. The ticket creation flow is clean, the schema is sensible, and the AI summary feature is a nice touch. It's pre-alpha in practice — no self-hosting docs, no SDK breadth, no tests — but the architecture is sound enough to build on.

christianalares/seventy-seven on GitHub
christianalares/seventy-seven