We built the helpdesk we wanted
Better Helpdesk is an open-source support inbox you install from npm into your own app. It runs in your process, in your database, and behind your login, and here is why we built it that way.
Better Helpdesk is an open-source support inbox you install from npm into your own app. It runs in your process, in your database, and behind your login, and here is why we built it that way.
We have used helpdesks for years, long before devguard existed: as developers who integrated them into products, and as the people answering customers in them. Intercom was the one we knew longest. So when devguard needed a helpdesk of its own, we knew what a good support inbox feels like from the agent's chair. We went looking, and did not find the one we wanted.
Intercom is good, and it is expensive. It charges per seat, so the bill grows with every colleague who should be able to answer a customer. The conversations sit on Intercom's servers.
Chatwoot is open source, and it is a second application to run. Self-hosted, it is a Rails app with Postgres, Redis, and Sidekiq, and its own login. You host and upgrade it, or you pay for Chatwoot Cloud, and either way you integrate it with your product so that it knows who your customers are.
Both stand next to your product. Neither knows what your product knows until you teach it, through an SDK, a webhook, or a sync.
We wanted something simple: a package to install, not a system to operate. A helpdesk for a small team should not need its own deploy pipeline.
We wanted it to grow with us. The tenth agent should cost what the first one did, and a feature we need next year should not sit behind a higher plan.
We wanted to own the data: the conversations in our own database, under our own backups, next to the customers they are about.
There are plenty of helpdesks, and we did not set out to add one more for its own sake. But building products is our trade, and we have spent years answering customers in other people's helpdesks. We knew what we were missing, and we knew how to build it.
Better Helpdesk is the result. You install it from npm into the Next.js app you already have, and it runs there: in your process, in your database, behind your login.
The difference is the one between adding a room to your house and renting an office across town. The office needs its own keys and its own heating. The room uses the boiler you already have.
In code, the room is three files:
// app/api/helpdesk/[...slug]/route.ts: every helpdesk route, one handlerimport { helpdesk } from "@/lib/helpdesk";const handle = (request: Request) => helpdesk.handler(request);export { handle as GET, handle as POST, handle as PATCH, handle as PUT, handle as DELETE, handle as OPTIONS,};
// app/helpdesk/[[...slug]]/page.tsx: the inbox, for your support teamimport { HelpdeskAdmin } from "better-helpdesk/admin";export default () => ( <HelpdeskAdmin api="/api/helpdesk" basePath="/helpdesk" locale="en" />);
// app/layout.tsx: the widget, for your customersimport { HelpdeskWidget } from "better-helpdesk/widget";<HelpdeskWidget inbox="support" locale="en" />;
A fourth, lib/helpdesk.ts, builds the helpdesk from your database connection and your session. One line in next.config lets Next.js compile the package, and one migrate command creates the tables.
Your session. You write one function, identify(request), that reads your app's own session and says who the person is and whether they are on your support team. There are no agent accounts to create and no seats to invite. A signed-in customer opens the widget and it already knows them, and shows them their earlier conversations.
Your database. Conversations, contacts, and companies are plain tables in the database you already run and back up: a helpdesk schema on Postgres and SQL Server, a helpdesk_ prefix on MySQL and SQLite. Support data joins your own tables, so on Postgres a report is a query:
select o.plan, count(*) as open
from helpdesk.conversation c
join helpdesk.company co on co.id = c.company_id
join organizations o on o.id::text = co.external_org_id -- your table
where c.status = 'open'
group by o.plan;What the customer saw. A bug report arrives with the page URL, the viewport, the locale, your app's version, and the last JavaScript errors the page threw. The form lists each item, and the customer can leave any of them out. A resolveContext function adds what your own tables know about their organization, such as the plan and the seats in use, and the agent sees it beside the thread.
Your design. The widget and the inbox read --helpdesk-* CSS custom properties, so they take your colors, your font, and your radius. The widget's theme="auto" follows the operating system's dark mode.
A function call. Your server code imports the helpdesk and calls it. A billing webhook can open a conversation when a payment fails; an upgrade can land on the customer's timeline. There is no REST client and no network hop. A webhook carries no session, so there you sign a short-lived identity token for the customer.
None of this matters if the inbox is poor to work in, and that was the part we had opinions about after years in other people's.
Conversations sit in one or several inboxes, for example support and sales, each with its own reply promise. Every conversation has a reference a customer can read out on the phone, such as ACME-1042, a priority, tags, and an assignee. An agent can snooze a conversation until Thursday, answer with a canned reply, leave an internal note that mentions a colleague, and work through the queue from the keyboard. If two agents open the same conversation, each sees that the other is there.
Email works in both directions. Replies go out through your own sender, and a customer's answer by email is threaded back into the conversation, with DKIM verified on the way in.
For teams with commitments to keep, there are service levels: response and resolution targets per customer tier and priority, with a clock that pauses while the customer has the ball. The Statistics page shows how often the targets were met.
Next to the inbox is a small CRM. Contacts and companies come from your app's own identity, so nobody types them in, and each has a timeline, a lead stage, and custom fields.
Most helpdesks have a rule builder: a screen of dropdowns that says "when a bug is urgent, tell Slack". Better Helpdesk has a function instead. onEvent in your config is called with every new conversation, every message, and every change, and what happens next is your own code:
onEvent: async event => { const { conversation } = event; if (event.kind !== 'conversation.created') return; if (conversation.type !== 'bug' || conversation.priority !== 'urgent') return; await fetch(process.env.SLACK_WEBHOOK_URL!, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ text: `Urgent bug ${helpdesk.reference(conversation)}: ${conversation.subject}`, }), });},
The rule lives in your repository, goes through a pull request like any other change, and can import anything your app can. Assigning by plan, opening an issue in your tracker, and paging someone on a missed service level are the same function with a different body.
What it leaves out is as deliberate as what it has, and the roadmap says so in writing.
It will not have its own login, because your users are already signed in. It will not have a hosted mode: it ships where your app ships, and we never see your data. It will not charge per seat or keep a feature behind a paid tier; it is MIT, all of it. And it will not let an AI answer your customers. AI can triage a conversation and draft a reply for an agent, and every reply a customer reads was sent by a person with a name.
It also has fewer channels than the applications it is compared with. Messages come in through the widget and by email, and that is the list. If you need WhatsApp in your support inbox, this is the wrong tool.
The first version went to npm on 28 September 2026, 1.0.0 followed on 22 October, and the current release is 1.2.0. Beyond what is described above, it has roles (agent, supervisor, viewer), an audit log, and file attachments. The interface is in English, German, French, and Italian. The package has four runtime dependencies: kysely, zod, mailparser, and mailauth.
We run it in production at devguard, where it was built. That is the only production deployment we can name, and you should weigh it as what it is: the authors using their own tool.
The demo at better-helpdesk.com/demo needs no install and no account. Write in as a customer, switch roles, answer as the agent, and then look at the rows in Postgres at the bottom of the page.
After that, we would like to know one thing: what would stop you from using it? Open an issue on GitHub and say so.
Angelo runs strategy and the relationships behind devguard. He spends his time with the teams who live with compliance day to day, and keeps what we build tied to what they actually need.
Start free, bring your frameworks, and keep the evidence attached as the work ships. No setup friction, no audit-week fire drill.
Start free, bring your frameworks, and keep the evidence attached as the work ships. No setup friction, no audit-week fire drill.