No description
  • TypeScript 99.3%
  • Dockerfile 0.7%
Find a file
2026-03-16 23:55:53 -06:00
scripts add db migrations 2026-03-16 23:25:12 -06:00
src files setup 2026-03-16 23:51:20 -06:00
.dockerignore bunch of auth and server stuff 2026-03-16 23:00:55 -06:00
.gitignore bunch of auth and server stuff 2026-03-16 23:00:55 -06:00
CLAUDE.md agents file 2026-03-16 23:55:53 -06:00
docker-compose.yml files setup 2026-03-16 23:51:20 -06:00
Dockerfile bunch of auth and server stuff 2026-03-16 23:00:55 -06:00
package.json files setup 2026-03-16 23:51:20 -06:00
pnpm-lock.yaml files setup 2026-03-16 23:51:20 -06:00
pnpm-workspace.yaml bunch of auth and server stuff 2026-03-16 23:00:55 -06:00
README.md files setup 2026-03-16 23:51:20 -06:00
tsconfig.json bunch of auth and server stuff 2026-03-16 23:00:55 -06:00

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, and MINIO_KMS_SECRET_KEY must be set to strong values in production.

Generate a secure MINIO_KMS_SECRET_KEY with:

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:

  1. Run the generator:
pnpm migration:create add_messages_table
  1. Implement the up function in the generated file under src/db/migrations/.

  2. 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.