Files
2026-02-17 17:30:45 -05:00

32 KiB

PRD: API Service

Module: api/ | Namespace: ajet.chat.api.* Status: v1 | Last updated: 2026-02-17


1. Overview

The API service is the stateless REST backend — the only service (besides Auth GW) with direct PostgreSQL access. It handles all data reads/writes and publishes events to NATS after mutations. All other services interact with data through the API.

2. Architecture

Auth Gateway → API Service → PostgreSQL
                           → NATS (publish events)
                           → MinIO (file storage)
  • No direct client access — all requests arrive via Auth GW with validated session context
  • Request headers from Auth GW:
    • X-User-Id: <uuid> — authenticated user
    • X-User-Role: <owner|admin|member> — role in the community context
    • X-Community-Id: <uuid> — current community (for community-scoped endpoints)
    • X-Trace-Id: <uuid> — request tracing

3. Database Schema

3.1 Migrations (Migratus, sequential)

001-create-users.up.sql
002-create-oauth-accounts.up.sql
003-create-communities.up.sql
004-create-community-members.up.sql
005-create-channel-categories.up.sql
006-create-channels.up.sql
007-create-channel-members.up.sql
008-create-messages.up.sql
009-create-attachments.up.sql
010-create-reactions.up.sql
011-create-mentions.up.sql
012-create-notifications.up.sql
013-create-sessions.up.sql
014-create-api-users.up.sql
015-create-api-tokens.up.sql
016-create-webhooks.up.sql
017-create-invites.up.sql
018-add-search-indexes.up.sql
019-create-bans.up.sql
020-create-mutes.up.sql
021-create-oauth-providers.up.sql
022-create-system-settings.up.sql

3.2 Tables

-- Core entities
users (id uuid PK, username text UNIQUE, display_name text, email text, avatar_url text, status_text text, created_at timestamptz)
oauth_accounts (id uuid PK, user_id uuid FKusers, provider text, provider_user_id text, created_at timestamptz, UNIQUE(provider, provider_user_id))
communities (id uuid PK, name text, slug text UNIQUE, created_at timestamptz)
community_members (community_id uuid FK, user_id uuid FK, role text CHECK(owner/admin/member), nickname text, avatar_url text, joined_at timestamptz, PK(community_id, user_id))
channel_categories (id uuid PK, community_id uuid FK, name text, position int)
channels (id uuid PK, community_id uuid FK NULL, category_id uuid FK NULL, name text, type text CHECK(text/dm/group_dm), visibility text CHECK(public/private), topic text, created_at timestamptz)
channel_members (channel_id uuid FK, user_id uuid FK, joined_at timestamptz, last_read_message_id uuid, PK(channel_id, user_id))
messages (id uuid PK, channel_id uuid FK, user_id uuid FK, parent_id uuid FKmessages NULL, body_md text, created_at timestamptz, edited_at timestamptz NULL)
attachments (id uuid PK, message_id uuid FK, filename text, content_type text, size_bytes bigint, storage_key text)
reactions (message_id uuid FK, user_id uuid FK, emoji text, created_at timestamptz, PK(message_id, user_id, emoji))
mentions (id uuid PK, message_id uuid FK, target_type text CHECK(user/channel/here), target_id uuid NULL)
notifications (id uuid PK, user_id uuid FK, type text CHECK(mention/dm/thread_reply/invite/system), source_id uuid, read boolean DEFAULT false, created_at timestamptz)
webhooks (id uuid PK, community_id uuid FK, channel_id uuid FK, name text, avatar_url text, token_hash text, created_by uuid FK, created_at timestamptz)
invites (id uuid PK, community_id uuid FK, created_by uuid FK, code text UNIQUE, max_uses int NULL, uses int DEFAULT 0, expires_at timestamptz NULL, created_at timestamptz)

-- Indexes
idx_messages_channel_created ON messages(channel_id, created_at)
idx_messages_parent ON messages(parent_id) WHERE parent_id IS NOT NULL
idx_messages_search ON messages USING GIN(to_tsvector('english', body_md))
idx_notifications_user_unread ON notifications(user_id, created_at) WHERE read = false
idx_channel_members_user ON channel_members(user_id)
idx_community_members_user ON community_members(user_id)

-- Ban/mute enforcement
bans (community_id uuid FK, user_id uuid FK, reason text, banned_by uuid FK, created_at timestamptz, PK(community_id, user_id))
mutes (community_id uuid FK, user_id uuid FK, expires_at timestamptz, muted_by uuid FK, created_at timestamptz, PK(community_id, user_id))
idx_mutes_expires ON mutes(expires_at) WHERE expires_at IS NOT NULL

-- OAuth providers (runtime-configurable)
oauth_providers (id uuid PK, provider_type text CHECK(github/gitea/oidc), name text, client_id text, client_secret_encrypted text, base_url text NULL, issuer_url text NULL, enabled boolean DEFAULT true, created_at timestamptz, updated_at timestamptz)
idx_oauth_providers_type ON oauth_providers(provider_type)

-- System settings (key-value for deployment-wide config)
system_settings (key text PK, value jsonb, updated_at timestamptz)

3.3 Ban & Mute Enforcement

Bans:

  • Ban record in bans table prevents user from:
    • Sending messages in any channel of the community
    • Joining channels
    • Accepting invites to the community
  • Banned user is removed from all channels and community membership on ban
  • Ban check runs in middleware for all community-scoped API endpoints
  • Bans are permanent until explicitly lifted by Admin+

Mutes:

  • Mute record in mutes table with expires_at timestamp
  • Muted user cannot:
    • Send messages (POST to message endpoints returns 403)
    • Add reactions
    • Send typing indicators
  • Muted user CAN still read messages and channels
  • Expired mutes are ignored (no cleanup needed — checked on read)
  • Duration specified as interval: 10m, 1h, 24h, 7d

4. API Endpoints

4.1 Communities

Method Path Description Auth
POST /api/communities Create community (first-user bootstrap or new) User
GET /api/communities List user's communities User
GET /api/communities/:id Get community details Member
PUT /api/communities/:id Update community name/slug Owner
DELETE /api/communities/:id Delete community Owner

POST /api/communities

Request:  {"name": "My Team", "slug": "my-team"}
Response: {"id": "uuid", "name": "My Team", "slug": "my-team", "created_at": "..."}
Side effects:
  - Creates community
  - Adds requesting user as owner
  - Creates #general channel (text, public)
  - Publishes :community/created to chat.events.{id}

Test Cases:

ID Test Description
COM-T1 Create community POST creates community, user is owner, #general exists
COM-T2 Slug uniqueness POST with existing slug returns 409
COM-T3 Slug format validation Slug with spaces/uppercase returns 422
COM-T4 List communities GET returns only communities user is a member of
COM-T5 Get community (member) GET returns community details
COM-T6 Get community (non-member) GET returns 403
COM-T7 Update community (owner) PUT updates name/slug
COM-T8 Update community (admin) PUT returns 403 (owner-only)
COM-T9 Delete community (owner) DELETE removes community and all associated data
COM-T10 Delete community (non-owner) DELETE returns 403

4.2 Channels

Method Path Description Auth
GET /api/communities/:cid/channels List channels in community Member
POST /api/communities/:cid/channels Create channel Admin+
GET /api/channels/:id Get channel details Channel member (or community member if public)
PUT /api/channels/:id Update channel Admin+
DELETE /api/channels/:id Delete channel Admin+
POST /api/channels/:id/join Join channel (public only) Member
POST /api/channels/:id/leave Leave channel Member
GET /api/channels/:id/members List channel members Channel member

POST /api/communities/:cid/channels

Request:  {"name": "backend", "type": "text", "visibility": "public", "category_id": "uuid or null"}
Response: {"id": "uuid", "name": "backend", ...}
Side effects:
  - Creates channel
  - Adds creator as channel member
  - Publishes :channel/created to chat.events.{community-id}

Test Cases:

ID Test Description
CH-T1 Create public channel POST creates channel, creator is member, event published
CH-T2 Create private channel POST with visibility=private works
CH-T3 Create channel (member role) POST returns 403 for non-admin
CH-T4 Duplicate channel name POST with existing name in same community returns 409
CH-T5 List channels GET returns public channels + private channels user is in
CH-T6 Join public channel POST /join adds user to channel, publishes :member/joined
CH-T7 Join private channel POST /join returns 403 (must be invited)
CH-T8 Leave channel POST /leave removes membership, publishes :member/left
CH-T9 Leave last channel Leaving #general is allowed
CH-T10 Delete channel DELETE removes channel, all messages, publishes :channel/deleted
CH-T11 Update channel topic PUT with new topic, publishes :channel/updated
CH-T12 Assign category PUT with category_id moves channel to category

4.3 Channel Categories

Method Path Description Auth
GET /api/communities/:cid/categories List categories (ordered) Member
POST /api/communities/:cid/categories Create category Admin+
PUT /api/categories/:id Update category (name, position) Admin+
DELETE /api/categories/:id Delete category (channels become uncategorized) Admin+

Test Cases:

ID Test Description
CAT-T1 Create category POST creates with position, returns category
CAT-T2 Reorder categories PUT with new position reorders correctly
CAT-T3 Delete category Channels in deleted category become uncategorized (category_id = null)
CAT-T4 List categories with channels GET returns categories with nested channel list

4.4 Messages

Method Path Description Auth
GET /api/channels/:id/messages List messages (paginated) Channel member
POST /api/channels/:id/messages Send message Channel member
GET /api/messages/:id Get single message Channel member
PUT /api/messages/:id Edit message (1-hour window) Author
DELETE /api/messages/:id Delete message Author or Admin+
GET /api/messages/:id/thread Get thread replies Channel member

Pagination: Cursor-based using message UUID.

GET /api/channels/:id/messages?before=<uuid>&limit=50
GET /api/channels/:id/messages?after=<uuid>&limit=50
Default limit: 50, max: 100

POST /api/channels/:id/messages

Request:  {"body_md": "hello @<user:uuid> check #<channel:uuid>", "parent_id": null}
Response: {"id": "uuid", "channel_id": "...", "user_id": "...", "body_md": "...", "created_at": "..."}
Side effects:
  - Creates message row
  - Parses mentions → creates mention rows
  - Creates notification rows for mentioned users
  - Publishes :message/created to appropriate NATS subject
  - If thread reply: notifies thread participants

PUT /api/messages/:id (edit)

Request:  {"body_md": "updated text"}
Response: {"id": "uuid", ..., "edited_at": "..."}
Validation:
  - User must be the author
  - created_at must be within 1 hour of now
  - Re-parses mentions, updates mention rows
  - Publishes :message/edited

Test Cases:

ID Test Description
MSG-T1 Send message POST creates message, returns with ID and timestamps
MSG-T2 Send message to channel user is not in POST returns 403
MSG-T3 Send empty message POST with empty body returns 422
MSG-T4 Send message with mentions Mention rows created, notifications created for mentioned users
MSG-T5 Send message with @here Notifications created for all online channel members
MSG-T6 Send thread reply POST with parent_id creates threaded message, notifies thread participants
MSG-T7 Thread does not nest POST with parent_id pointing to another reply is rejected (or parent_id resolves to root)
MSG-T8 Get messages (default pagination) GET returns last 50 messages, newest last
MSG-T9 Get messages (before cursor) GET with ?before=uuid returns older messages
MSG-T10 Get messages (after cursor) GET with ?after=uuid returns newer messages
MSG-T11 Edit message within window PUT succeeds within 1 hour, sets edited_at
MSG-T12 Edit message after window PUT returns 403 after 1 hour
MSG-T13 Edit message (non-author) PUT returns 403
MSG-T14 Edit updates mentions Editing to add @user creates new mention + notification
MSG-T15 Delete own message DELETE removes message, publishes :message/deleted
MSG-T16 Delete other's message (admin) DELETE succeeds for admin/owner
MSG-T17 Delete other's message (member) DELETE returns 403
MSG-T18 Get thread GET /thread returns all replies to a message, ordered by created_at
MSG-T19 NATS event published After send, :message/created event appears on correct subject
MSG-T20 DM message event Message in DM channel publishes to chat.dm.{channel-id}
MSG-T21 Community message event Message in community channel publishes to chat.events.{community-id}

4.5 Reactions

Method Path Description Auth
PUT /api/messages/:id/reactions/:emoji Add reaction (idempotent) Channel member
DELETE /api/messages/:id/reactions/:emoji Remove reaction Reactor
GET /api/messages/:id/reactions List reactions on message Channel member

Test Cases:

ID Test Description
RXN-T1 Add reaction PUT creates reaction, publishes :reaction/added
RXN-T2 Add duplicate reaction PUT is idempotent, no error
RXN-T3 Remove reaction DELETE removes, publishes :reaction/removed
RXN-T4 Remove non-existent reaction DELETE returns 404
RXN-T5 List reactions GET returns reactions grouped by emoji with user lists
RXN-T6 Multiple users same emoji Both reactions stored, count shows 2

4.6 DMs (Global)

Method Path Description Auth
GET /api/dms List user's DM channels User
POST /api/dms Create or get existing DM User
POST /api/dms/group Create group DM User

POST /api/dms

Request:  {"user_id": "uuid"}
Response: {"id": "channel-uuid", "type": "dm", "members": [...]}
Behavior: Returns existing DM if one exists between the two users, otherwise creates new.

POST /api/dms/group

Request:  {"user_ids": ["uuid1", "uuid2", ...]}
Response: {"id": "channel-uuid", "type": "group_dm", "members": [...]}

Test Cases:

ID Test Description
DM-T1 Create DM POST creates DM channel with both users as members
DM-T2 Get existing DM POST for existing pair returns same channel
DM-T3 Create group DM POST /group creates group_dm with all specified users
DM-T4 List DMs GET returns all DM/group_dm channels for user, ordered by last message
DM-T5 DM with self POST with own user_id returns 422
DM-T6 Group DM minimum POST /group with < 2 other users returns 422

4.7 Users & Profiles

Method Path Description Auth
GET /api/me Get current user User
PUT /api/me Update profile (display_name, status_text) User
GET /api/users/:id Get user profile User
GET /api/communities/:cid/members List community members Member
PUT /api/communities/:cid/members/:uid Update member (nickname, role) Admin+ (role: Owner only)
DELETE /api/communities/:cid/members/:uid Remove/kick member Admin+

Test Cases:

ID Test Description
USR-T1 Get current user GET /me returns authenticated user's profile
USR-T2 Update display name PUT /me with display_name updates it
USR-T3 Update status text PUT /me with status_text updates it
USR-T4 Get user profile GET /users/:id returns public profile
USR-T5 List community members GET returns all members with roles and nicknames
USR-T6 Set community nickname PUT /members/:uid with nickname (admin)
USR-T7 Change member role (owner) Owner can promote member to admin
USR-T8 Change member role (admin) Admin cannot change roles (403)
USR-T9 Kick member (admin) DELETE removes member, publishes :member/kicked
USR-T10 Kick owner Cannot kick owner (403)

4.8 Notifications

Method Path Description Auth
GET /api/notifications List notifications (paginated) User
POST /api/notifications/read Mark notifications as read User
GET /api/notifications/unread-count Get unread count User

Pagination: ?after=<uuid>&limit=50&unread=true

Test Cases:

ID Test Description
NTF-T1 Get notifications GET returns notifications newest-first
NTF-T2 Filter unread GET with ?unread=true returns only unread
NTF-T3 Mark read POST with notification IDs marks them read
NTF-T4 Unread count GET /unread-count returns integer count
NTF-T5 Notification on mention @mention in message creates notification
NTF-T6 Notification on DM New DM message creates notification
NTF-T7 Notification on thread reply Reply to your thread root creates notification

4.9 Presence & Typing

Method Path Description Auth
POST /api/heartbeat Report online status User
GET /api/communities/:cid/presence Get online members Member

POST /api/heartbeat

Response: 200 OK (immediate)
Side effects:
  - Async: update last_seen_at in DB
  - Async: publish :presence/online to chat.presence.{community-id} for each community

Presence logic:

  • Online: last heartbeat < 2 minutes ago
  • Offline: last heartbeat >= 2 minutes ago
  • Clients send heartbeat every 60 seconds

Test Cases:

ID Test Description
PRS-T1 Heartbeat returns 200 immediately POST /heartbeat responds fast (< 50ms)
PRS-T2 Heartbeat updates last_seen DB shows updated last_seen_at after heartbeat
PRS-T3 Online presence User with recent heartbeat appears in presence list
PRS-T4 Offline after timeout User without heartbeat for 2+ minutes not in online list
PRS-T5 Presence event published Heartbeat publishes to NATS presence subject

Method Path Description Auth
GET /api/search Global search User

Parameters:

?q=search+term          — required, search query
&type=messages|channels|users  — optional filter (default: all)
&community_id=uuid      — optional, scope to community
&channel_id=uuid        — optional, scope to channel
&from=uuid              — optional, filter by author
&after=datetime         — optional, date range start
&before=datetime        — optional, date range end
&cursor=uuid&limit=20   — pagination

Behavior:

  • Messages: PostgreSQL tsvector full-text search, only in channels user has access to
  • Channels: name substring match within user's communities
  • Users: username/display_name substring match

Test Cases:

ID Test Description
SRC-T1 Search messages by keyword Returns matching messages user can access
SRC-T2 Search respects access Messages in channels user isn't in are excluded
SRC-T3 Search in specific channel channel_id filter narrows results
SRC-T4 Search by author from filter returns only that user's messages
SRC-T5 Search channels type=channels returns matching channel names
SRC-T6 Search users type=users returns matching usernames/display names
SRC-T7 Empty query Returns 422
SRC-T8 Pagination Cursor-based pagination returns next page
SRC-T9 Date range filter after/before narrows message results

4.11 Invites

Method Path Description Auth
POST /api/communities/:cid/invites Create invite link Admin+
GET /api/communities/:cid/invites List active invites Admin+
DELETE /api/invites/:id Revoke invite Admin+
POST /api/invites/:code/accept Accept invite (join community) User
POST /api/communities/:cid/invites/direct Direct invite (by user ID) Admin+

POST /api/communities/:cid/invites

Request:  {"max_uses": 10, "expires_in_hours": 48}   — both optional
Response: {"id": "uuid", "code": "abc123", "url": "https://chat.example.com/invite/abc123", ...}

Test Cases:

ID Test Description
INV-T1 Create invite link POST creates invite with unique code
INV-T2 Accept invite POST /accept adds user to community as member
INV-T3 Accept expired invite POST /accept returns 410 (gone)
INV-T4 Accept max-uses exhausted POST /accept returns 410 after max uses reached
INV-T5 Accept invite (already member) POST /accept returns 200 (idempotent)
INV-T6 List invites GET returns active (non-expired, non-exhausted) invites
INV-T7 Revoke invite DELETE invalidates the invite
INV-T8 Direct invite POST /direct creates invite + notification for target user
INV-T9 Create invite (member role) POST returns 403

4.12 Webhooks (Incoming)

Method Path Description Auth
POST /api/communities/:cid/webhooks Create webhook Admin+
GET /api/communities/:cid/webhooks List webhooks Admin+
DELETE /api/webhooks/:id Delete webhook Admin+
POST /api/webhooks/:id/incoming Post via webhook Webhook token

POST /api/webhooks/:id/incoming

Headers: Authorization: Bearer <webhook-token>
Request: {"content": "Build #42 passed!", "username": "CI Bot", "avatar_url": "..."}
Response: 204 No Content
Side effects: Creates message in webhook's channel, publishes event

Test Cases:

ID Test Description
WH-T1 Create webhook POST creates webhook with token, returns token once
WH-T2 Post via webhook POST /incoming creates message in channel
WH-T3 Post with invalid token Returns 401
WH-T4 Post with custom username Message shows webhook's custom username
WH-T5 Delete webhook DELETE removes webhook, token becomes invalid
WH-T6 List webhooks GET returns webhooks (without tokens)

4.13 Slash Commands

Method Path Description Auth
POST /api/commands Execute slash command User

Request:

{"command": "/kick @<user:uuid>", "channel_id": "uuid", "community_id": "uuid"}

Commands v1:

Command Args Permission Description
/help [command] All Show help or specific command help
/topic <text> Admin+ Set channel topic
/nick <nickname> All Set community nickname
/invite [max_uses] [expires_hours] Admin+ Generate invite link
/kick @<user> Admin+ Kick user from community
/ban @<user> Admin+ Ban user from community
/mute @<user> <duration> Admin+ Mute user for duration
/token create <name> [scopes] Owner Create API token
/token revoke <name> Owner Revoke API token
/token list Owner List API tokens
/webhook create <name> [channel] Admin+ Create incoming webhook
/webhook delete <name> Admin+ Delete webhook
/webhook list Admin+ List webhooks
/status <text> All Set status text

Test Cases:

ID Test Description
CMD-T1 /help returns command list All commands listed with descriptions
CMD-T2 /topic sets channel topic Channel topic updated, event published
CMD-T3 /topic (non-admin) Returns 403
CMD-T4 /nick sets nickname Community member nickname updated
CMD-T5 /kick removes user User removed from community, event published
CMD-T6 /kick (non-admin) Returns 403
CMD-T7 /ban bans user User banned (cannot rejoin)
CMD-T8 /mute mutes user User cannot send messages for duration
CMD-T9 /token create Creates API token, returns token string once
CMD-T10 /webhook create Creates webhook, returns webhook URL + token
CMD-T11 Unknown command Returns error message suggesting /help
CMD-T12 /status sets status text User's status_text updated

4.14 File Upload

Method Path Description Auth
POST /api/channels/:id/upload Upload image Channel member

Request: Multipart form data with image file. Constraints: Images only (JPEG, PNG, GIF, WebP). Max size: 10MB.

Response: {"id": "uuid", "filename": "screenshot.png", "content_type": "image/png", "size_bytes": 12345, "url": "/files/uuid/screenshot.png"}

Flow:

  1. Validate file type and size
  2. Upload to MinIO with storage key attachments/{uuid}/{filename}
  3. Return attachment metadata (to be referenced in message)

Test Cases:

ID Test Description
UPL-T1 Upload JPEG File stored in MinIO, attachment record created
UPL-T2 Upload PNG Same as above
UPL-T3 Upload non-image Returns 422 (only images allowed)
UPL-T4 Upload too large Returns 413 (> 10MB)
UPL-T5 Upload to channel user isn't in Returns 403

4.15 Read Tracking

Method Path Description Auth
POST /api/channels/:id/read Mark channel as read Channel member

Request:

{"last_read_message_id": "uuid"}

Updates channel_members.last_read_message_id. Used by SMs to calculate unread counts.

Test Cases:

ID Test Description
RD-T1 Mark read POST updates last_read_message_id
RD-T2 Unread count derivation Messages after last_read_message_id are "unread"
RD-T3 Mark read (non-member) Returns 403

4.16 Admin: OAuth Provider Management

Method Path Description Auth
GET /api/admin/oauth-providers List all OAuth providers Owner
POST /api/admin/oauth-providers Create OAuth provider Owner
PUT /api/admin/oauth-providers/:id Update OAuth provider Owner
DELETE /api/admin/oauth-providers/:id Delete OAuth provider Owner

POST /api/admin/oauth-providers

Request:  {"provider_type": "github", "name": "GitHub", "client_id": "...", "client_secret": "...", "enabled": true}
Response: {"id": "uuid", "provider_type": "github", "name": "GitHub", "client_id": "...", "enabled": true, "created_at": "..."}
Note: client_secret is encrypted at rest and never returned in responses.

PUT /api/admin/oauth-providers/:id

Request:  {"name": "GitHub Org", "client_id": "...", "client_secret": "...", "enabled": false}
Response: {"id": "uuid", "provider_type": "github", "name": "GitHub Org", ...}
Note: Omitting client_secret from the request leaves it unchanged.

Test Cases:

ID Test Description
AOP-T1 Create OAuth provider POST creates provider, returns without client_secret
AOP-T2 List providers GET returns all providers without secrets
AOP-T3 Update provider PUT updates name/enabled, secret unchanged if omitted
AOP-T4 Delete provider DELETE removes provider
AOP-T5 Non-owner access Non-owner user returns 403
AOP-T6 Invalid provider_type POST with unknown type returns 422
AOP-T7 Duplicate provider_type Allowed (multiple GitHub providers for different orgs)

5. Cross-Cutting Concerns

5.1 Error Response Format

{"error": {"code": "NOT_FOUND", "message": "Channel not found", "details": {}}}

5.2 Rate Limiting

  • Applied at Auth GW level, not in API service itself
  • API trusts that Auth GW has already rate-limited

5.3 NATS Publishing

  • Every mutation endpoint publishes an event to the appropriate NATS subject
  • Events are fire-and-forget (API does not wait for subscriber acknowledgment)
  • Event publishing failure should be logged but not fail the HTTP response

5.4 Audit Trail (P2)

  • Log all admin actions (kick, ban, role change, channel delete) with actor + target + timestamp
  • Queryable by Owner via future admin API

6. Service Configuration

6.1 Config Shape

{:server {:host "0.0.0.0" :port 3001}
 :db     {:host "localhost" :port 5432 :dbname "ajet_chat"
          :user "ajet" :password "..." :pool-size 10
          :migrations {:enabled true :location "migrations"}}
 :nats   {:url "nats://localhost:4222"
          :stream-name "ajet-events"
          :publish-timeout-ms 5000}
 :minio  {:endpoint "http://localhost:9000"
          :access-key "minioadmin" :secret-key "minioadmin"
          :bucket "ajet-chat"}
 :limits {:max-message-length 4000        ;; characters
          :max-upload-size    10485760     ;; 10MB in bytes
          :edit-window-minutes 60
          :default-page-size  50
          :max-page-size      100}}

6.2 Middleware Pipeline

Requests flow through middleware in this order:

1. Ring defaults (params, cookies, multipart)
2. Request logging (method, path, start time)
3. Exception handler (catch-all → 500 JSON error)
4. Trace ID extraction (X-Trace-Id → MDC)
5. User context extraction (X-User-Id, X-User-Role, X-Community-Id → request map)
6. Ban check (community-scoped: reject if user is banned)
7. Mute check (write endpoints: reject if user is muted)
8. Reitit routing → handler
9. Response logging (status, duration)

6.3 Startup / Shutdown Sequence

Startup:

1. Load config (EDN + env vars)
2. Create DB connection pool (HikariCP)
3. Run Migratus migrations (if enabled)
4. Connect to NATS
5. Connect to MinIO, ensure bucket exists
6. Start http-kit server
7. Log "API service started on port {port}"

Shutdown (graceful):

1. Stop accepting new HTTP connections
2. Wait for in-flight requests (max 30s)
3. Close NATS connection
4. Close DB connection pool
5. Log "API service stopped"

6.4 Health Check

Method Path Auth Description
GET /api/health None Service health status

Response (200):

{"status": "ok", "checks": {"db": "ok", "nats": "ok", "minio": "ok"}}

Response (503 — degraded):

{"status": "degraded", "checks": {"db": "ok", "nats": "error", "minio": "ok"}}

Test Cases:

ID Test Description
HLT-T1 Health check all up Returns 200 with all checks "ok"
HLT-T2 Health check DB down Returns 503 with db check "error"
HLT-T3 Health check NATS down Returns 503 with nats check "error"
HLT-T4 Health check MinIO down Returns 503 with minio check "error"

7. Migration SQL (Outlines)

7.1 Migration Naming Convention

{NNN}-{description}.up.sql     — forward migration
{NNN}-{description}.down.sql   — rollback migration

Migrations are sequential and must be applied in order. Each migration is idempotent — re-running a completed migration is a no-op (handled by Migratus tracking table).

7.2 Key Migration Notes

  • 001-create-users: users table + unique index on username + unique index on email
  • 006-create-channels: channels table, community_id is nullable (DMs have NULL)
  • 008-create-messages: messages table + composite index (channel_id, created_at) for pagination
  • 010-create-reactions: Composite PK (message_id, user_id, emoji) — one reaction per user per emoji per message
  • 018-add-search-indexes: to_tsvector('english', body_md) GIN index on messages for full-text search
  • 019-create-bans: bans table with composite PK (community_id, user_id)
  • 020-create-mutes: mutes table with composite PK (community_id, user_id), index on expires_at
  • 021-create-oauth-providers: oauth_providers table for runtime-configurable OAuth providers
  • 022-create-system-settings: system_settings key-value table for deployment-wide settings (e.g., setup_completed)