- TypeScript 99.3%
- Dockerfile 0.7%
| scripts | ||
| src | ||
| .dockerignore | ||
| .gitignore | ||
| CLAUDE.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package.json | ||
| pnpm-lock.yaml | ||
| pnpm-workspace.yaml | ||
| README.md | ||
| tsconfig.json | ||
serpent-chat-backend
Express + TypeScript backend for Serpent Chat.
Stack
- Runtime: Node 22
- Framework: Express 5
- Database: PostgreSQL 17
- Language: TypeScript 5 (ES2024)
- Auth: JWT (Bearer token)
Getting Started
Prerequisites
Run with Docker Compose
docker compose up
The API will be available at http://localhost:3000.
Run locally
pnpm install
# Set environment variables (see below)
pnpm dev
Environment Variables
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
Port the server listens on |
DATABASE_URL |
— | PostgreSQL connection string |
JWT_SECRET |
change-me |
Secret used to sign JWT tokens |
MINIO_ENDPOINT |
minio |
MinIO hostname |
MINIO_PORT |
9000 |
MinIO API port |
MINIO_ACCESS_KEY |
minioadmin |
MinIO access key |
MINIO_SECRET_KEY |
minioadmin |
MinIO secret key |
MINIO_USE_SSL |
false |
Use HTTPS for MinIO connections |
MINIO_KMS_SECRET_KEY |
minio-default-key:AAAA... |
KMS key for server-side encryption. Format: key-name:base64-encoded-32-byte-key |
Note
:
JWT_SECRET,MINIO_ACCESS_KEY,MINIO_SECRET_KEY, andMINIO_KMS_SECRET_KEYmust be set to strong values in production.Generate a secure
MINIO_KMS_SECRET_KEYwith:echo "my-key-name:$(openssl rand -base64 32)"All three MinIO buckets have AES-256 server-side encryption enabled by default. The KMS key is required for this to work — MinIO will reject encryption requests if it is not set.
Scripts
| Command | Description |
|---|---|
pnpm dev |
Start dev server with hot reload |
pnpm build |
Compile TypeScript to dist/ |
pnpm start |
Run compiled output |
pnpm migration:create <name> |
Scaffold a new migration file |
Project Structure
src/
features/
auth/ # Registration & login
users/ # User profiles
chat/
servers/ # Servers (invite-only communities)
channels/ # Channels within servers
middleware/
auth.ts # JWT requireAuth middleware
types/
express.d.ts # Express Request augmentation (req.userId)
db/
migrate.ts # Migration runner
migrations/ # Migration files (001_*, 002_*, ...)
tables/ # SQL table definitions
db.ts # Postgres connection pool
routes.ts # Top-level router
index.ts # App entry point
Migrations
Migrations run automatically on startup. Applied migrations are tracked in the migrations table.
Migration files are named YYYYMMDDHHmmss_description.ts to avoid conflicts when multiple people create migrations simultaneously.
To add a new migration:
- Run the generator:
pnpm migration:create add_messages_table
-
Implement the
upfunction in the generated file undersrc/db/migrations/. -
Register it in
src/db/migrations/index.ts:
import * as addMessagesTable from "./20260316143000_add_messages_table";
const migrations: Migration[] = [initialSchema, addMessagesTable];
Each migration runs in its own transaction and is recorded by name. It will run exactly once on the next startup.
API
All endpoints require an Authorization: Bearer <token> header unless marked public.
Auth
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register |
No | Register a user |
| POST | /api/auth/login |
No | Login |
Register / Login body:
{ "email": "user@example.com", "username": "user", "password": "secret" }
Response:
{ "token": "<jwt>" }
Users
| Method | Path | Description |
|---|---|---|
| GET | /api/users/:id |
Get user profile |
Servers
Servers are invite-only. Users only see servers they are members of.
| Method | Path | Description |
|---|---|---|
| GET | /api/servers |
List servers the current user belongs to |
| POST | /api/servers |
Create a server |
| GET | /api/servers/:serverId |
Get a server |
| POST | /api/servers/:serverId/members |
Add a user to a server (owner/admin) |
Create server body:
{ "name": "My Server", "iconId": "<optional-icon-id>" }
Add member body:
{ "userId": "<user-id>" }
Channels
Channels belong to a server. Public channels are visible to all server members. Private channels require explicit membership.
| Method | Path | Description |
|---|---|---|
| GET | /api/servers/:serverId/channels |
List accessible channels |
| POST | /api/servers/:serverId/channels |
Create a channel |
| POST | /api/servers/:serverId/channels/:channelId/members |
Add a user to a private channel (owner/admin) |
Create channel body:
{ "name": "general", "topic": "optional topic", "isPrivate": false }
Add channel member body:
{ "userId": "<user-id>" }
Files
Uploads use multipart/form-data with the file in a field named file.
| Method | Path | Limit | Description |
|---|---|---|---|
POST |
/api/files/avatar |
5MB | Upload a user avatar (returns public URL) |
POST |
/api/files/server-icon/:serverId |
5MB | Upload a server icon (returns public URL) |
POST |
/api/files/channel/:channelId |
10MB | Upload an attachment to a channel (returns 24-hour presigned URL) |
GET |
/api/files/:fileId |
— | Get a 24-hour presigned URL for any file |
All file URLs are 24-hour presigned URLs. Channel membership is enforced server-side on every request for attachments. Avatars and server icons only require authentication.