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: Google OAuth and 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 (v22 or higher)
- pnpm (v10.12.4)
pnpm installWrangler 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 session encryptionGOOGLE_ID: Google OAuth client IDGOOGLE_SECRET: Google OAuth client secretGOOGLE_REDIRECT_URI: Google OAuth redirect URI (e.g.http://localhost:8787/auth/google)E2E_GMAIL_ACCOUNT: Gmail account for E2E testingRESEND_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 the Wrangler console instead of sending real emails.
mlack uses Cloudflare D1 (SQLite) via Drizzle ORM. For local development, wrangler manages a local D1 instance automatically.
# Apply migrations to local D1
pnpm db:migrate
# Seed the #general channel
pnpm db:seed
# Generate new migration files after schema changes
pnpm db:generate
# Open Drizzle Studio to view/edit data
pnpm db:studio# Start development server (wrangler dev)
pnpm dev
# Build the project
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:
npx wrangler login
-
Create the D1 database:
npx wrangler d1 create mlack-db
Copy the
database_idfrom the output and updatewrangler.toml:[[d1_databases]] binding = "DB" database_name = "mlack-db" database_id = "<YOUR_DATABASE_ID>"
-
Apply migrations to the remote database:
pnpm db:migrate:prod
-
Seed the remote database:
pnpm db:seed:prod
-
Set production secrets:
npx wrangler secret put SESSION_SECRET --env production npx wrangler secret put GOOGLE_ID --env production npx wrangler secret put GOOGLE_SECRET --env production npx wrangler secret put GOOGLE_REDIRECT_URI --env production npx wrangler secret put RESEND_API_KEY --env production npx wrangler secret put RESEND_FROM_EMAIL --env production
For
GOOGLE_REDIRECT_URI, use your production URL (e.g.https://mlack.<your-subdomain>.workers.dev/auth/google). Make sure this URL is also added as an Authorized redirect URI in the Google Cloud Console under APIs & Services > Credentials, by clicking on your OAuth 2.0 Client ID.For
RESEND_API_KEY, create an API key at Resend. ForRESEND_FROM_EMAIL, use a verified domain in your Resend account (e.g.noreply@yourdomain.com). -
Deploy:
pnpm run deploy
Production deployments are automated via GitHub Actions. Pushing to the prod branch triggers the deployment pipeline defined in .github/workflows/deploy.yml.
Pipeline overview:
testjob — Runs unit tests, linter, and full builde2ejob — Runs Playwright E2E tests (in parallel withtest)deployjob — Applies D1 migrations and deploys to Cloudflare Workers (only runs after bothtestande2epass)
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 Wrangler 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, E2E_GMAIL_ACCOUNT, and E2E_GMAIL_PASSWORD secrets 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:prodmlack/
├── e2e/ # Playwright E2E tests
├── hono/ # Application source
│ ├── auth/ # Password hashing, email verification utilities
│ ├── components/ # Server-rendered JSX pages + CSS
│ ├── db/ # Drizzle schema, connection, migrations
│ ├── durableObjects/ # Cloudflare Durable Objects
│ │ └── ChatRoom.ts # WebSocket server with hibernation
│ ├── routes/ # Route handlers
│ ├── static/ # Client-side TypeScript
│ ├── app.tsx # App factory, middleware, route registration
│ ├── index.ts # Entry point (exports app + Durable Objects)
│ ├── testApp.ts # Test helper with mock session
│ └── types.ts # Shared types (User, Bindings, Variables)
├── wrangler.toml # Cloudflare Workers configuration
├── package.json # Dependencies and scripts
├── playwright.config.ts # Playwright configuration
├── tsconfig.json # TypeScript configuration
├── vitest.config.ts # Vitest configuration
└── biome.json # Biome linter/formatter configuration
- Runtime: Cloudflare Workers
- Framework: Hono
- WebSocket: Cloudflare Durable Objects with WebSocket Hibernation API
- Database: Cloudflare D1 (SQLite) with Drizzle ORM
- Language: TypeScript
- Testing: Vitest for unit tests, Playwright for E2E tests
- Linting: Biome
- Package Manager: pnpm
Chat interface. Redirects to /auth/login if not authenticated.
Health check endpoint.
{ "status": "ok", "message": "Service is running" }Retrieve message history for a channel (requires authentication).
List all channels (requires authentication).
Create a new channel (requires authentication).
Join a channel (requires authentication).
Leave a channel (requires authentication).
Real-time messaging endpoint. Requires an authenticated session. Messages are routed through a Cloudflare Durable Object for reliable broadcasting across all connected clients.
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.
This project uses OpenCode with MCP servers for Cloudflare and Chrome DevTools. After setting up OpenCode, authenticate with each Cloudflare MCP server:
opencode mcp auth cloudflare-docs
opencode mcp auth cloudflare-bindings
opencode mcp auth cloudflare-observability
opencode mcp auth cloudflare-radar