Chatpack

Quickstart

A working chat backend - with live delivery - in five minutes.

This walks you from npm install to a live, real-time chat backend. Every step works as written; swap the demo pieces (memory storage, cookie auth) for production pieces when you're ready.

Prefer learning from a complete app? examples/messenger is a full 1:1 messenger - sidebar, live messages, read receipts - in vanilla HTML+JS with a step-by-step tutorial README.

Install

Both packages are needed - @chatpack/core is the engine, @chatpack/adapter-memory is the storage it plugs into:

npm install @chatpack/core @chatpack/adapter-memory

Bun note: if Bun's supply-chain guard (minimumReleaseAge) is enabled, versions published in the last 24h are skipped and Bun silently resolves an older release. Check with npm view @chatpack/core dist-tags.

Add the client (optional)

For a browser or React frontend, add the first-party client:

npm install @chatpack/client react
import { createChatClient } from "@chatpack/client/react";

export const chatClient = createChatClient({
  // Omit baseURL when the frontend and handler share an origin.
  credentials: "include",
});

The client uses the server's existing cookie session. It does not implement login, logout, user lookup, or token handling. See the client guide for REST, realtime, React, and plugin usage.

Create your chat server

lib/chat.ts
import { chatpack } from "@chatpack/core";
import { memoryAdapter } from "@chatpack/adapter-memory";

export const chat = chatpack({
  storage: memoryAdapter(),
  // resolve the current user from a request - the ONLY auth touchpoint.
  auth: async (req) => {
    const session = await getSessionFromCookie(req.headers.get("cookie"));
    return session ? { id: session.userId } : null;
  },
});

The auth hook must return ChatpackUser | null - an object with at least { id: string } (extra fields are allowed and ignored), or null for unauthenticated requests. Returning a bare string is treated as unauthenticated and every request will get a 401.

Prefer cookie-based sessions over Authorization headers: the browser sends cookies automatically on every request - including the SSE stream in step 5, where custom headers are impossible. See Authentication for demo cookies, iframe-proof attributes, and hybrid bearer-token setups.

The hook receives a raw Web-standard Request - there is no request.cookies helper. A minimal demo auth that parses the cookie header itself:

// demo auth: a plain cookie naming the user (swap for your auth library)
auth: (request) => {
  const cookie = request.headers.get("cookie") ?? "";
  const id = /(?:^|;\s*)demo_user=([^;]+)/.exec(cookie)?.[1] ?? null;
  return id ? { id: decodeURIComponent(id) } : null;
},

Mount the API

The route file must be a catch-all ([...chatpack] in Next.js) - Chatpack serves many sub-paths under basePath (default /api/chat).

app/api/chat/[...chatpack]/route.ts
import { chat } from "@/lib/chat";
export const { GET, POST, PATCH, DELETE } = chat.handler();

Never hand-write your own message or stream routes. The one handler already serves every route

  • conversations, messages, read-state, plugins, and the SSE stream. Custom /api/messages-style routes split state and break live delivery.

Your chat backend is now live at /api/chat. More frameworks (TanStack Start, Express, plain Node) in Framework guides.

Call it over HTTP

Find-or-create a conversation (the authenticated user + otherUserId):

curl -X POST /api/chat/conversations \
  -H 'content-type: application/json' \
  -d '{"otherUserId": "bob"}'
{
  "conversation": {
    "id": "conv_1",
    "pairKey": "alice:bob",
    "createdAt": "2026-07-22T19:47:47.945Z",
    "metadata": {},
    "participants": [
      { "conversationId": "conv_1", "userId": "alice", "joinedAt": "…", "lastReadMessageId": null },
      { "conversationId": "conv_1", "userId": "bob", "joinedAt": "…", "lastReadMessageId": null }
    ]
  }
}

Send a message - note the text field is body:

curl -X POST /api/chat/conversations/conv_1/messages \
  -H 'content-type: application/json' \
  -d '{"body": "hey bob!"}'

List history (newest first, keyset-paginated):

curl '/api/chat/conversations/conv_1/messages?limit=50'

Chatpack never owns a users table, so it cannot check that otherUserId actually exists - a typo silently creates a conversation with a ghost user. Validate recipient ids against your own users table before calling.

Go live in the browser

const events = new EventSource("/api/chat/stream");

// TypeScript: custom event names fall outside EventSourceEventMap, so the
// listener parameter is typed `Event` - cast to MessageEvent for `.data`.
events.addEventListener("message.created", (e) => {
  const { message } = JSON.parse((e as MessageEvent).data);
  // render it - reconnection & missed-message backfill are automatic
});

If the connection drops, EventSource reconnects with Last-Event-ID and Chatpack replays whatever was missed from storage - durable-first delivery, no lost messages. Details in Real-time with SSE.

Or call it straight from server code

// find-or-create a 1:1 conversation between two users
const conversation = await chat.api.getOrCreateConversation({
  userId: "alice",
  otherUserId: "bob",
});

// send a message
await chat.api.sendMessage({
  userId: "alice",
  conversationId: conversation.id,
  body: "hey bob!",
});

// read the history
const { messages } = await chat.api.listMessages({
  userId: "bob",
  conversationId: conversation.id,
});

That's it. Only the two participants can read or write - enforced by default, customizable via the permissions hooks.

Going to production

Swap the storage line for Postgres - everything else stays the same:

lib/chat.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { drizzleAdapter } from "@chatpack/adapter-drizzle";

export const chat = chatpack({
  storage: drizzleAdapter(drizzle(process.env.DATABASE_URL!)),
  auth: async (req) => getSessionUser(req),
});

See Drizzle / Postgres for table creation and serverless drivers, and Deployment for where SSE and the memory adapter do (and don't) work.

Next steps

On this page