Betaaccepting early access
// Features

Everything it does. And where it all lives.

A ticket desk, a help center, a form builder, a customer portal and an API — running on a database, a bucket and a mail account you own. Every line below is something the product does today.

Bring your own infrastructure

The same three connections the desk has always run on — plus the operational care that makes them safe to hand over.

A connection string you can act on

Both mongodb:// and mongodb+srv:// are understood, including several seed hosts. Every rejection is a 400 that explains itself — missing host, malformed SRV, bad scheme — because this is a value a person types, and "invalid connection string" is how you end up with a ticket.

Connections are leased, not held

One pool per organization: five sockets, nothing kept warm, a five-second server-selection timeout so a slow cluster fails its own request instead of tying up a worker, an idle eviction, and an LRU ceiling that bounds the process no matter how many organizations exist.

Bytes never transit the API

Agents upload with a presigned PUT and read with a five-minute presigned GET. HavnDesk stores a name, a type, a size and an object key — nothing else — and file names are sanitized against path traversal before a key is built.

Prove the keys before you save them

Settings → File storage has a Check button that writes and deletes a probe object, so a wrong key fails on the settings screen rather than the first customer screenshot.

Recover what Postmark still holds

Postmark keeps attachment content for 30 days. Connect a bucket late and the panel tells you how many files are still recoverable, recovers them in the background, and reports what came back alongside what is permanently gone. A cron nudges organizations that never open the screen.

The one credential we must be able to send

Your Postmark token has to be used, not just verified, so it is the one secret stored reversibly — encrypted with AES-256-GCM under a deployment key and write-only through the API. Rotating that key re-encrypts everything without anyone re-entering a token.

The queue your team works in

Everything a support desk needs, with the parts that are usually fixed made yours.

Statuses with your words on them

Five canonical effects — new, open, pending, resolved, closed — decide what the plumbing does. How many stages exist, what they are called and what color they are is yours. Rename Pending to "Waiting on customer" for free; delete it for free too, because a ticket keeps its effect and simply falls back to the canonical label.

Tags that decide who may read

A tag can be restricted to an audience, and a ticket carrying it is hidden from everyone outside it. With two restricted tags visibility is the union — and an unrestricted tag never compensates for a restricted one.

Route mail by tag

An inbound alias such as billing@your-inbound-domain files straight onto that tag. Only recipients on your own inbound domain are matched, so a customer Cc'ing their own billing address cannot steer their own ticket.

SLA deadlines frozen at creation

Per-priority first-response and resolution targets are stamped onto each ticket when it opens, which is what makes "show me everything about to breach" an indexed query instead of a scan. States are live: on track, at risk, breaching, breached.

First response, measured honestly

The first-response clock stops at the first public agent reply or the first move off new. An internal note does not count, because the customer did not get an answer.

Waiting on customer answers itself

One flag, set when the team is waiting. It clears when the customer writes back — by email, in the portal or in the app — rather than when an agent remembers to clear it.

Mentions that reach people

@email, @local-part, @Full Name or @First notifies exactly the people named and nobody else. It is the one notification that ignores a mute, because being named is not the same as subscribing.

Live, without a refresh

Ticket events stream to the console over server-sent events, so a comment from a colleague lands in the open thread. It is one process today; the docs say so plainly.

Search, filters, and the right default order

Filter by status, priority, company, tag, assignee and SLA state; sort by created or by last activity — which is what you want once inbound mail is on, since a reply does not change a ticket's creation date. Cursor pagination, so page two never shifts under you.

A trail per ticket

Status changes, assignments and replies are recorded with whoever made them, on the ticket they belong to. The help-center side has its own record: article views, wired to the same event bus that feeds the queue.

Your help center

The part of support that arrives before the question does — hosted on your domain, wearing your brand, not ours.

Two versions of the same article, in one document

A runbook has a team version — internal hostnames, the workaround not ready to promise, why the last outage happened — and a customer version. Kept together they stay in step; kept apart, the customer page gets followed during an incident and turns out to be wrong.

Draft, published, and internal-only

A draft is team-visible only: the public index and article routes filter it out exactly like an internal article. "Internal" is a separate switch from "the customer section is empty", because never publishing and not having written it yet are different intentions.

A front page you compose

Headline, intro, whether search and the article list show, which categories appear, an accent and light/dark/system — plus your own sections in four anchor slots, so switching the search off does not move anything else.

Your brand bar, not our logo

The help center carries your logo, or your name where no logo is uploaded — never the HavnDesk mark. The accent hero picks its own foreground from the accent's luminance, so yellow still gets readable black and cobalt still gets white.

Search that doesn't ship your articles

Queries run server-side. A knowledge base that filtered in the browser would send every article to every reader, including the ones they are not allowed to see.

Topics as a filter, not a maze

Categories are Metro tiles that narrow the list below them, so the whole help center stays on one address and one scroll position instead of scattering across pages nobody returns to.

Yours at your own address

Claim the domain the help center answers on. Two independent proofs have to line up — a TXT record you publish and the domain attaching to our edge — and the admin is shown the exact records to copy, with the status read back. Two organizations cannot claim the same name.

Forms and the customer portal

Three ways for a customer to reach you that aren't email, all of them yours and none of them requiring an account.

A form per kind of request

String, multiline, boolean or dropdown questions. A field can be tied to the ticket's subject, its body or its email — and the email field is required, because a ticket nobody can reply to is not a ticket.

Each form has its own address and its own embed

Every form answers at /forms/<slug> on your own domain and carries a one-line widget tag of its own, so "report a bug" and "ask about billing" can live in two different places and still file into one queue.

Forms compile into tickets, not into the model

Answers become the subject, body and tags of an ordinary ticket, which is why a form can change shape without the ticket ever growing a field. A form is turned off rather than deleted — links are already in the world — and tickets created before that keep their answers.

The customer portal, without a download

Somebody who filed one ticket should not have to install anything. The portal is server-rendered HTML on the same host as the help center, working with cookies and forms alone — no JavaScript, no bundle, no build step.

Passwordless, and narrowly scoped

A one-time code goes to the ticket's own address. What the cookie can then do is exactly four things: list the tickets that address submitted, read one, reply to one, sign out. Status changes are not offered — support keeps the steering wheel.

Values that land where they are needed

A tag declares which fields a kind of ticket has, an agent fills them in once, and the result is a link that opens a help-center article with those values already substituted. Nobody pastes a one-time password into a chat window again.

Those values are credentials, and treated as such

Encrypted at rest, never returned by any API, decrypted only at the moment of rendering. The link is per ticket, expiring and rotatable, and it substitutes into one chosen article — so a link shared in the wrong place renders blank instead of handing over a password.

Mail in and out

The embarrassing parts of email support — threading, bounces, complaints — handled rather than hoped about.

A reply is a comment, not a new ticket

Inbound mail is matched back to its ticket by the [HavnDesk #N] subject tag, then the RFC 5322 References chain, then the sender's most recent ticket. Quoted history and the signature are stripped, the ticket moves up on activity, and a resolved or closed ticket reopens.

One webhook URL per organization

A token in the path both authenticates the caller and identifies the organization. Sending mail to the wrong organization's URL is not a mistake the system can make.

Always 200 once it's genuine

Mail that is understood and deliberately ignored still answers 200, so Postmark is never told to retry something we meant to drop — and a retry that does arrive is deduplicated rather than filed twice.

Outbound is a real email, recorded

An agent can compose and send from the ticket. It leaves through your Postmark as ordinary mail and is written onto the thread, so the customer's reply threads back to where it started.

Bounces and complaints, per organization

A permanent bounce suppresses the address and flags its open tickets; a spam complaint is always permanent. The ticket is kept and kept open — an undeliverable address is a data problem for a human, not a reason to lose the conversation.

The suppression list you can actually read

Settings → Undeliverable addresses shows every address that bounced or complained and whether it is really blocked, with a resume action that un-flags its tickets. This list is safety-critical: it is the only thing standing between you and mailing a stranger who already told you to stop.

Loop prevention

Automated replies, our own outbound and mail from the help address itself are recognized and not answered, so two autoresponders cannot have a conversation on your dime.

The widget on your site

One script tag, no CORS, and a key that is public on purpose.

One line, and it cannot touch your page

The bundle is isolated in a shadow DOM, so your styles cannot break it and it cannot break yours. It is fetched from our origin as a plain script, which is why installing it needs no CORS configuration at all.

Wearing your colors

Accent, surface, text, corner radius, which corner it sits in and the launcher's title are per organization, with data-attributes on the tag overriding them when you want a one-off.

The key is public, deliberately

The browser key can only create tickets and is constrained by an origin allowlist you control. The server key — which can read and write — is a different key with a different scope, and it never goes near a page.

Signed, always

Widget submissions always require an HMAC signature over the exact request bytes, with a five-minute replay window. Signing happens on your server; the secret does not exist in the browser.

Cached at the edge, and safe to be

The bundle is content-stable and served with an ETag, so it is a 304 for every visitor after the first and a CDN can hold it hard without ever shipping stale support code.

Forms, inline

A custom form can be embedded with its own tag, so a whole questionnaire lands in the widget rather than opening a page that looks nothing like the site it is on.

Who works the desk

Sign-in, roles and notification defaults that assume a real team rather than a demo tenant.

Passwordless to start

A six-digit code, hashed at rest, ten-minute life, five attempts, rate-limited by address and IP. There is no password field in the schema at all, which is the point.

Microsoft 365 sign-in when you want it

Entra ID over OIDC with PKCE — no client secret exists anywhere, because the flow is built for a public client and the server's only job is verification. The signature, the audience and the issuer are all checked; personal Microsoft accounts do not qualify.

Four roles with the floors written down

Owner does everything including granting owner. Admin invites and manages members and keys. Agent works tickets and manages companies and tags. Member reads, creates, comments and closes — and that is all.

Invitations, not signups

There is no public signup page to expose. A revoked invite is kept rather than deleted, so a revoked code can never be replayed.

Notifications you actually choose

Per-person: mute, five per-category toggles, and a watch mode of all, assigned or mentions. The agent who owns a ticket hears about it; a busy day does not become a busy inbox.

Claim your people by email domain

Register the domains your staff write from and prove ownership with a DNS TXT token. It is independent of the domain your support mail flows through, because who your people are and where your replies come from are different questions.

API, billing and the platform behind it

The parts you only notice when they are missing: keys, invoices, an audit log and a way to run it all.

An API designed to be leaked

Keys are scoped, hashed at rest, shown once and revocable. Sign submissions with HMAC-SHA256 over the exact request bytes and a leaked key stops being enough on its own — the caller also needs a secret that never leaves your server.

Tags behave by who is calling

A company slug nobody has seen before becomes a company on the ticket — that is what happens the first time a product files a bug. Tags are stricter, and deliberately differently strict: your server may introduce a new tag name as an unrestricted label, while the browser widget may only use tags the team already created and gets a 400 for anything else. Only the trusted path can grow your vocabulary.

One contract, one reference

Request bodies are zod schemas in a shared package that the API validates against, the console type-checks against, and the generated OpenAPI document is built from. Renaming a field breaks the build, not production.

Billing that behaves like yours

A volume price list — price per band of seats — with per-organization custom tiers, grandfathered pricing that survives a global change, invoices carrying the service period they bought, and refunds issued against the payment. Stripe is optional: an install without payments still prices, runs and refunds, with the money collected outside.

An admin console and an audit log

Platform administrators can find any organization, see its usage and which integrations are connected, edit the price list without a deploy, and rotate the encryption key with a preview of what would change. Every admin action is recorded.

Built to be operated

Configuration is validated at boot so a deploy cannot look healthy while a feature is broken; liveness and readiness are separate endpoints so a database outage drains instances instead of restart-looping them; logs are structured and correlated by request id; indexes are reconciled at boot and can be reviewed before they change.

iOS, Android and the web you host

One console, three targets. The web build is a static export with no server runtime, which is why the same artifact can be served from several domains — including the one your help center answers on.

Bring your stack, get the desk.

Accounts are set up with us today. Send a ticket from the support page and a real engineer will answer — through HavnDesk, naturally.

Request access