Vibe code your own Intercom

Vibe code your own Intercom as a help button and a shared inbox. Routing, the customer graph, and product tours are the other product. Here is the split.

  • teardown
  • vibe-coding
  • scope

You can vibe code your own Intercom. Start with a help button and a shared inbox. The catalog calls Intercom that loop, not a revenue platform.

You pay $74–$139 a person a month. The card says that price scales with the team. The verdict is “Build only your support loop”.

Earlier today I wrote about building a Canny alternative. That was a regular backlog post. This one is a teardown of the public architecture and which half you leave alone.

Vibe code your own Intercom as a help button and an inbox

The Intercom page splits the work into three rows.

The first row is 4 days. In-app help button, shared inbox, replies. That is a bubble in the product, a list of conversations, and a composer. You can vibe-code this.

The second row is 2 weeks and up. Assignment, notes, tags, customer context, email notifications. Still one inbox.

The third row is 3 months and up. AI agents, product tours, omnichannel, a marketing platform. Skip that row.

If you tell an agent to “clone Intercom”, it aims at the third row. The useful v1 is the first one.

Here is the shape of the data. Nothing else has to exist at 4 days.

const conversation = {
  id: 'conv-slot-login',
  customerId: 'U-ada',
  state: 'open',
  assigneeId: null,
  version: 1,
}

const message = {
  conversationId: 'conv-slot-login',
  type: 'comment',
  authorId: 'U-ada',
  body: 'The Slot login page 500s after a password reset.',
  seq: 1,
  clientKey: 'ck-ada-01',
}

A customer, a conversation, messages with a type and a sequence. Assignment and notes can wait a week.

The audience line on the page is a small SaaS team supporting authenticated customers. If that is you, a button and an inbox are enough. If the company already lives in Intercom’s outbound campaigns and Fin, keep paying.

The same v1 also replaces Crisp, LiveChat, and Drift. Help Scout is the email cousin at $25–$75 a month. Chatbase is the FAQ bot at $40–$150.

How does Intercom work

Intercom publishes a REST API at https://api.intercom.io and a JavaScript Messenger. I am summarizing those docs and two engineering posts, not reconstructing their database.

You create a thread with POST /conversations. The create conversation endpoint takes a contact and a body. HTML is not supported on that write. You reply with POST /conversations/{id}/reply. A teammate note uses message_type: "note". A customer-visible reply uses message_type: "comment". That split is in the reply docs.

A retrieved conversation returns conversation parts. Each part has a part_type, a body, and optional assignment fields. The API hard-limits a retrieve to 500 parts. Intercom states include open, closed, and snoozed. The catalog’s v1 uses open, waiting, and closed.

The Messenger boots with Intercom('boot', { app_id, user_id, email }). Their JavaScript methods also list startConversation, which sends immediately. A vibe-coded button needs none of those names. Persist the message, then show it.

Real-time delivery is a separate product they named Nexus. Mel Spath and Drago Crnjac’s January 8, 2018 post Making some wiggle room for engineering innovations is the public map. Nexus routed hundreds of millions of messages a week. The Java version used about 30GB of memory per instance and sat on 280 AWS instances. They rewrote it in Go. The write-up says Nexus-Go used about 10GB, dropped average latency from 80ms to 7ms, and ran on 50 instances.

Treat “Nexus-Go is still the 2026 path” as an educated guess. The product rule is still one conversation, one sequence, persist before you broadcast.

The Messenger itself is a React app. Intercom’s Reducing The Intercom Messenger Bundle Size By 65% says frame.js used to ship the whole thing as one file, almost 600kb gzipped, later split down to 240kb. A 4-day v1 is a small widget on your origin. One Durable Object per conversation, one D1 database. The catalog’s stack is Astro, Durable Objects, and D1. Use that, or Rails if that is your weekday language.

SIGNED IDENTITY JWT or HMAC, never the browser CUSTOMER MESSAGE Persist, then assign seq SHARED INBOX Open, waiting, closed One row per conversation REPLY OR NOTE Notes never hit the widget WIDGET SERIALIZES Allowlist of comment types
The public path: verify identity, persist the message, reply or note, serialize only comments to the customer.

How Intercom identity verification works

The catalog’s first hard rule is the same as Intercom’s public one. Do not trust a user_id the browser typed.

Intercom’s current docs, Secure your Messenger, recommend a server-signed JWT. The payload includes user_id. Optional fields include email and an expiry. You sign with HS256 and the Messenger secret. Without that token, a stranger can impersonate Ada and read her history.

The older feature was Identity Verification. You hashed user_id or email with HMAC-SHA256 and sent user_hash. Intercom’s migration guide still accepts those hashes, and says JWTs add expiry and signed attribute updates. Their help article on that older flow, updated August 21, 2026, is marked deprecated.

The mini-prompt on the page is “The widget that demands ID at the door”. Sign the payload on the host app. Verify the signature, a fresh timestamp, the audience for this workspace, and an origin on the allowlist before any conversation data flows. Fail any check and the widget gets a reload message, with no history.

function issueMessengerJwt(secret, customer) {
  return jwt.sign(
    {
      user_id: customer.id,
      email: customer.email,
      exp: Math.floor(Date.now() / 1000) + 3600,
    },
    secret,
    { algorithm: 'HS256' }
  )
}

function customerFromRequest(req, secret) {
  const payload = jwt.verify(req.token, secret, { algorithms: ['HS256'] })
  if (!payload.user_id) {
    throw new Error('missing-user')
  }
  return payload.user_id
}

Ada’s id comes from the verified payload. A query string she edits in DevTools is not a customer. Keep the secret off the frontend. Intercom’s troubleshooting page says the same: if you leaked the key, rotate it.

How Intercom assignment and notes work

Assignment is a conversation part. Intercom’s assign a conversation endpoint posts message_type: "assignment" with an admin_id and an assignee_id. You can assign a teammate, a team, or 0 to unassign. I would not rebuild their run_assignment_rules engine in week one. A dropdown that sets assigneeId is enough.

Intercom’s Inbox explained shows teammate avatars on a conversation that is already open, so two people do not write the same reply. Table view shows up to three avatars, then a +N count. You do not see your own. That needs the “Show teammates presence” setting.

The mini-prompt is “Two agents, one conversation, zero clobbering”. Give the conversation a version that increments on every reply, assignment, and state change. The second reply carries the version the agent loaded. If it no longer matches, reject and show what Maria sent 20 seconds ago.

async function reply(db, { conversationId, agentId, body, version, clientKey }) {
  return db.transaction(async (tx) => {
    const conversation = await tx.getConversation(conversationId)
    if (conversation.version !== version) {
      throw new Error('stale-conversation')
    }

    const existing = await tx.findByKey(conversationId, clientKey)
    if (existing) return existing

    const message = await tx.insertMessage({
      conversationId,
      authorId: agentId,
      type: 'comment',
      body,
      clientKey,
    })
    await tx.bumpVersion(conversationId)
    return message
  })
}

A retried send reuses clientKey. The other mini-prompt is “Messages that arrive once, in order”. The server assigns seq at write time. Clients render by sequence, because hotel wifi can deliver part 4 before part 3.

Notes are a type, not a flag. The mini-prompt is “Internal notes that physically can’t leak”. Intercom’s reply API already treats note as a different message_type from comment. Copy that. The widget serializer allowlists comment. A boolean internal is one forgotten WHERE away from leaking a note.

const CUSTOMER_TYPES = ['comment']

function serializeForWidget(messages) {
  return messages
    .filter((message) => CUSTOMER_TYPES.includes(message.type))
    .map((message) => ({
      id: message.id,
      body: message.body,
      seq: message.seq,
    }))
}

Notification emails and customer exports use the same allowlist. A new type starts invisible.

The customer graph and product tours

The skip row is Intercom the platform, not a bigger inbox.

Their help article Tracking user data in Intercom (updated August 24, 2026) stores three kinds of people data. Standard attributes they already collect. Custom attributes you send, such as plan or project count. Events for actions, with first time, last time, and a total. People and Company attributes share a name namespace.

That graph is why a teammate opens Ada’s thread and sees her plan without asking. It is also why Product Tours can fire on “signed up this week and still on trial”. Intercom’s automatically show your Product Tour page targets users, visitors, or leads with attributes, tags, and AND/OR rules. If two tours match, the most recently updated one wins.

Product Tours explained is the no-code overlay: multi-page steps, a page URL, a shareable link. The 2018 design post Building Product Tours wanted tours as reusable objects you insert in the Inbox, in messages, and in articles.

The catalog’s non-goal already names it. Do not build product tours, outbound marketing, AI agents, a CRM, or omnichannel social support.

A field or two on the customer panel is enough context. Plan, email, signed-up date. Canny is the board if the thread was a feature request, $79–$99 a month and 10 hours for votes and a status.

An Intercom alternative for a small support team

4-DAY V1 Help button in the app Shared inbox Replies that persist once SKIP THIS HALF People graph and tours AI agents and Fin Omnichannel and campaigns
Ship the button and the inbox. Leave tours, Fin, and outbound campaigns on the skip row.

I would rebuild Intercom for a three-person SaaS that currently pastes customer emails into a shared inbox and loses the plan name. Authenticated users, one product, one queue. Identity signed on the host app. Notes that cannot serialize to the widget. A version on each conversation so Maria and I cannot send the same fix twice.

I would host it next to the product, on Astro, Durable Objects, and D1, because that is the stack on the card. Use Rails if that is your weekday language. Add assignment once two people share a queue. I would not build a tour overlay or an AI agent.

If the rent is support email, that is Help Scout, $25–$75 a month, 3 days for inbound threading, assignment, and replies. If the rent is questions the docs already answer, that is Chatbase. If the team already lives in Intercom’s campaigns and Fin, keep paying.

A campfire for the team is still Slack. How Slack works is that split.

Start with the first row

Go to Build your own Intercom. Copy the AHA prompt, or the stack you already write.

Tell the agent the first estimate only: in-app help button, shared inbox, replies. Four days.

If the company already lives in the people graph and the tour builder, keep paying.