A real-time Slack-like chat app built with Hono, TypeScript, and Cloudflare Workers.
- Real-time Chat: WebSocket-powered chat with instant messaging via Cloudflare Durable Objects
- Channels: Create, join, and leave channels. Messages are scoped to channels
- Persistent Storage: Messages stored in Cloudflare D1 (SQLite) and persist across sessions
- Authentication: Email/password registration with email verification (6-digit OTP)
- WebSocket Hibernation: Cost-efficient WebSocket management that hibernates idle connections
- Health Check Endpoint:
/healthendpoint for monitoring - TypeScript: Full TypeScript with strict type checking
- Testing: Unit tests (Vitest) and E2E tests (Playwright)
- Linting: Biome for code quality and formatting
- Node.js (
^24.22.0or^26.12.0, as required by thecfCLI) - pnpm (v10.34.9, pinned via
packageManagerinpackage.json) - The
cfCLI (npm install -g cf@1.0.0-beta.26)
pnpm installcf dev reads secrets from .dev.vars. Copy the sample file and fill in your values:
cp .env.sample .dev.varsRequired variables in .dev.vars:
SESSION_SECRET: A secure secret for signing session cookies (HS256 JWT)RESEND_API_KEY: API key from Resend for sending verification emailsRESEND_FROM_EMAIL: Sender address for verification emails (e.g.noreply@yourdomain.com)
Note:
RESEND_API_KEYandRESEND_FROM_EMAILare only required in production. In local development (NODE_ENV=development), verification codes are logged to thecf devconsole instead of sending real emails.cf devalso prints a "Missing required secrets" warning for every secret that is absent from.dev.vars; it is harmless for the secrets you don't need locally.
mlack uses Cloudflare D1 (SQLite) with plain SQL queries. For local development, cf manages a local D1 instance automatically.
# Apply migrations to local D1
pnpm db:migrate
# Seed the #general channel
pnpm db:seed
# Create an empty migration file in hono/db/migrations, then write its SQL by hand
cf d1 migrations create <migration-name> --dir hono/db/migrations
# Query the local database
cf d1 raw 00000000-0000-4000-8000-000000000000 --local --persist-to .wrangler/state --sql "SELECT email, name FROM users"cf d1 addresses databases by ID. The local database uses the placeholder ID 00000000-0000-4000-8000-000000000000 from cloudflare.config.ts, and --persist-to .wrangler/state points cf d1 at the state used by cf dev.
# Start development server (cf dev)
pnpm dev
# Compile the client scripts into public/static and type-check everything
pnpm build
# Type-check only (no emit)
pnpm build:check
# Run unit tests in watch mode
pnpm test
# Run unit tests once
pnpm test:run
# Run E2E tests
pnpm test:e2e
# Lint code
pnpm lint
# Lint and auto-fix
pnpm lint:fix-
Authenticate with Cloudflare:
cf auth login
-
Create the D1 database:
cf d1 create --name mlack-db
Copy the database ID from the output into
PRODUCTION_DATABASE_IDincloudflare.config.ts, and into thedb:migrate:prodanddb:seed:prodscripts inpackage.json. -
Apply migrations to the remote database:
pnpm db:migrate:prod
-
Seed the remote database:
pnpm db:seed:prod
-
Deploy:
pnpm run deploy
-
Set production secrets (secrets belong to an existing Worker, so set them after the first deploy; later deploys leave them untouched):
cf workers secrets update SESSION_SECRET --worker mlack-production --type secret_text --text "<value>" cf workers secrets update RESEND_API_KEY --worker mlack-production --type secret_text --text "<value>" cf workers secrets update RESEND_FROM_EMAIL --worker mlack-production --type secret_text --text "<value>"
The app responds with errors until
SESSION_SECRETis set. ForRESEND_API_KEY, create an API key at Resend. ForRESEND_FROM_EMAIL, use a verified domain in your Resend account (e.g.noreply@yourdomain.com).
Production deployments are automated via GitHub Actions. Pushing to the prod branch triggers the deployment pipeline defined in .github/workflows/deploy.yml.
Pipeline overview:
cijob β Runs the CI workflow (.github/workflows/ci.yml): unit tests, linter and build, plus Playwright E2E tests in paralleldeployjob β Applies D1 migrations and deploys to Cloudflare Workers (only runs aftercipasses)
Typical workflow:
# Merge main into prod to trigger a deployment
git checkout prod
git merge main
git push origin prodRequired GitHub secrets:
The deploy job requires two repository secrets. Add them under Settings > Secrets and variables > Actions > New repository secret:
CLOUDFLARE_API_TOKENβ Create a token in the Cloudflare dashboard using the "Edit Cloudflare Workers" template, then add D1 Edit permission to it. The template includes Workers Scripts Write, Account Settings Read, User Memberships Read, and other permissions thecfCLI needs.CLOUDFLARE_ACCOUNT_IDβ Your Cloudflare account ID. Find it on the Workers & Pages overview page in the right sidebar.
The E2E job reuses the existing SESSION_SECRET secret already configured for CI.
You can still deploy manually if needed:
pnpm run deployIf you've added new database migrations, apply them before deploying:
pnpm db:migrate:prod- Runtime: Cloudflare Workers
- Framework: Hono
- WebSocket: Cloudflare Durable Objects with WebSocket Hibernation API
- Database: Cloudflare D1 (SQLite)
- Language: TypeScript
- Testing: Vitest for unit tests, Playwright for E2E tests
- Linting: Biome
- Package Manager: pnpm
pnpm test # Watch mode
pnpm test:run # Single runpnpm test:e2eE2E tests verify the full application flow including authentication, chat interface rendering, real-time WebSocket messaging, and message persistence.