ZapBB Forum Platform
Version: 1.0
Status: Draft
Date: January 20, 2026
Reference: PRD 34c32848.md
This document provides comprehensive API specifications for ZapBB, including endpoint definitions, request/response formats, authentication requirements, and error handling.
- Command-style for actions:
POST /threads/{id}:lock,POST /posts/{id}:approve - Resource CRUD where appropriate:
GET /categories,POST /threads,PATCH /posts/{id} - Consistent error responses: JSON error with
code,message,details - Pagination by default:
?limit=20&offset=0or cursor-based for large result sets - OpenAPI v3.1: Full schema coverage, auto-generated Swagger UI at
/api/v1/docs
- Base URL:
https://api.example.com/api/v1/ - Version in URL path:
/api/v1/,/api/v2/, etc. - Breaking changes require major version bump
- Deprecated endpoints include
X-Deprecatedheader with sunset date
- Backend uses
utoipato derive OpenAPI v3.1 schema from Rust types and route annotations. - Schema endpoint is served at
/api/v1/openapi.json. - Swagger UI is available at
/api/v1/docs.
- Tooling: OpenAPI-Qraft
- Output location:
frontend/lib/api/generated/ - Usage: Frontend imports generated hooks instead of manual fetches.
- API schema changes must update generated client code in the same PR.
- CI should validate that generated client code matches the OpenAPI schema.
- The Next.js web app uses NextAuth.js (Auth.js) sessions.
- Session cookies authenticate SSR and API calls originating from Next.js.
Headers:
Authorization: Bearer <jwt_token>
Token Structure:
{
"sub": "user_uuid",
"role": "member|moderator|admin",
"exp": 1234567890,
"iat": 1234567890
}| Permission | Description | Roles |
|---|---|---|
thread.create |
Create new threads | Member, Moderator, Admin |
thread.lock |
Lock/unlock threads | Moderator, Admin |
post.create |
Create posts/replies | Member, Moderator, Admin |
post.moderate |
Edit/delete any post | Moderator, Admin |
user.ban |
Ban users | Moderator, Admin |
admin.settings |
Modify site settings | Admin |
{
"data": { /* resource or collection */ },
"meta": {
"request_id": "uuid",
"timestamp": "2026-01-20T04:00:00Z"
}
}{
"data": [ /* items */ ],
"pagination": {
"limit": 20,
"offset": 0,
"total": 150,
"has_more": true
},
"meta": {
"request_id": "uuid",
"timestamp": "2026-01-20T04:00:00Z"
}
}{
"error": {
"code": "UNAUTHORIZED",
"message": "User must be authenticated to perform this action",
"details": {
"endpoint": "/api/v1/posts",
"action": "POST",
"timestamp": "2026-01-20T04:00:00Z"
}
}
}These endpoints are used for issuing JWTs to API clients and service integrations. The web app uses NextAuth.js session routes under the Next.js app (e.g., /api/auth/*).
Register a new user account.
Request:
{
"username": "john_doe",
"email": "john@example.com",
"password": "SecureP@ssw0rd"
}Response (201 Created):
{
"data": {
"user": {
"id": "uuid",
"username": "john_doe",
"email": "john@example.com",
"role": {
"id": "uuid",
"name": "Member"
},
"created_at": "2026-01-20T04:00:00Z"
},
"token": "jwt_token_here",
"refresh_token": "refresh_token_here"
}
}Validation Rules:
username: 3-50 characters, alphanumeric + underscoreemail: Valid email formatpassword: Min 8 characters, must include uppercase, lowercase, number
Authenticate user and receive tokens.
Request:
{
"email": "john@example.com",
"password": "SecureP@ssw0rd"
}Response (200 OK):
{
"data": {
"user": {
"id": "uuid",
"username": "john_doe",
"email": "john@example.com",
"role": {
"id": "uuid",
"name": "Member"
}
},
"token": "jwt_token_here",
"refresh_token": "refresh_token_here"
}
}Invalidate current session.
Authorization: Required
Response (204 No Content)
Refresh JWT token using refresh token.
Request:
{
"refresh_token": "refresh_token_here"
}Response (200 OK):
{
"data": {
"token": "new_jwt_token_here",
"refresh_token": "new_refresh_token_here"
}
}List all categories (forums).
Query Parameters:
limit(int, default: 20): Number of results per pageoffset(int, default: 0): Pagination offsetparent_id(uuid, optional): Filter by parent category
Response (200 OK):
{
"data": [
{
"id": "uuid",
"name": "General Discussion",
"description": "Talk about anything here",
"slug": "general-discussion",
"parent_id": null,
"display_order": 0,
"is_locked": false,
"is_leaf": false,
"child_count": 3,
"thread_count": 125,
"post_count": 1450,
"created_at": "2026-01-01T00:00:00Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 15
}
}Get category details with permissions for current user.
Authorization: Optional (affects permission data)
Response (200 OK):
{
"data": {
"id": "uuid",
"name": "General Discussion",
"description": "Talk about anything here",
"slug": "general-discussion",
"parent_id": null,
"display_order": 0,
"is_locked": false,
"thread_count": 125,
"post_count": 1450,
"permissions": {
"can_view": true,
"can_create_thread": true,
"can_reply": true,
"can_moderate": false
},
"created_at": "2026-01-01T00:00:00Z"
}
}Create a new category.
Authorization: Required (Admin only)
Request:
{
"name": "New Category",
"description": "Description here",
"parent_id": null,
"display_order": 10
}Response (201 Created):
{
"data": {
"id": "uuid",
"name": "New Category",
"description": "Description here",
"slug": "new-category",
"parent_id": null,
"display_order": 10,
"is_locked": false,
"thread_count": 0,
"post_count": 0,
"created_at": "2026-01-20T04:00:00Z"
}
}Update category details.
Authorization: Required (Admin only)
Request:
{
"name": "Updated Name",
"description": "Updated description",
"display_order": 5
}Response (200 OK):
{
"data": {
"id": "uuid",
"name": "Updated Name",
"description": "Updated description",
"slug": "updated-name",
"display_order": 5,
"updated_at": "2026-01-20T04:00:00Z"
}
}List threads in a category.
Query Parameters:
limit(int, default: 20)offset(int, default: 0)sort_by(string):recent,popular,pinned(default:recent)
Response (200 OK):
{
"data": [
{
"id": "uuid",
"title": "Welcome to ZapBB!",
"slug": "welcome-to-zapbb",
"category_id": "uuid",
"author": {
"id": "uuid",
"username": "admin",
"profile": {
"avatar_url": "https://..."
}
},
"is_locked": false,
"is_pinned": true,
"is_sticky": false,
"reply_count": 25,
"view_count": 350,
"last_post_at": "2026-01-20T03:00:00Z",
"created_at": "2026-01-01T00:00:00Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 125
}
}Get thread details including first post.
Response (200 OK):
{
"data": {
"id": "uuid",
"title": "Welcome to ZapBB!",
"slug": "welcome-to-zapbb",
"category": {
"id": "uuid",
"name": "General Discussion",
"slug": "general-discussion"
},
"author": {
"id": "uuid",
"username": "admin"
},
"first_post": {
"id": "uuid",
"content": "Welcome everyone!",
"content_html": "<p>Welcome everyone!</p>",
"created_at": "2026-01-01T00:00:00Z"
},
"is_locked": false,
"is_pinned": true,
"reply_count": 25,
"view_count": 350,
"created_at": "2026-01-01T00:00:00Z"
}
}Get paginated posts in a thread.
Query Parameters:
limit(int, default: 20)offset(int, default: 0)
Response (200 OK):
{
"data": [
{
"id": "uuid",
"thread_id": "uuid",
"author": {
"id": "uuid",
"username": "user123",
"profile": {
"avatar_url": "https://...",
"custom_title": "Forum Veteran"
}
},
"content": "Great forum!",
"content_html": "<p>Great forum!</p>",
"reply_to_id": null,
"is_deleted": false,
"edited_at": null,
"reaction_count": 5,
"reactions": [
{ "emoji": "👍", "count": 3 },
{ "emoji": "❤️", "count": 2 }
],
"created_at": "2026-01-02T00:00:00Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 25
}
}Create a new thread.
Authorization: Required
Validation:
category_idmust reference a leaf category (a category with no children). Attempting to create a thread under a parent category will return a400 Bad Requesterror.
Request:
{
"title": "My New Thread",
"content": "This is the first post content...",
"category_id": "uuid (must be a leaf category)"
}Response (201 Created):
{
"data": {
"thread": {
"id": "uuid",
"title": "My New Thread",
"slug": "my-new-thread",
"category_id": "uuid",
"author_id": "uuid",
"created_at": "2026-01-20T04:00:00Z"
},
"post": {
"id": "uuid",
"thread_id": "uuid",
"content": "This is the first post content...",
"created_at": "2026-01-20T04:00:00Z"
}
}
}Update thread (title only).
Authorization: Required (Author or Moderator)
Request:
{
"title": "Updated Thread Title"
}Response (200 OK):
{
"data": {
"id": "uuid",
"title": "Updated Thread Title",
"slug": "updated-thread-title",
"updated_at": "2026-01-20T04:00:00Z"
}
}Soft-delete a thread.
Authorization: Required (Author or Moderator)
Response (204 No Content)
Lock a thread (moderator action).
Authorization: Required (Moderator or Admin)
Request:
{
"reason": "Off-topic discussion"
}Response (200 OK):
{
"data": {
"thread": {
"id": "uuid",
"is_locked": true,
"updated_at": "2026-01-20T04:00:00Z"
},
"mod_log": {
"id": "uuid",
"action": "thread_lock",
"reason": "Off-topic discussion",
"created_at": "2026-01-20T04:00:00Z"
}
}
}Unlock a thread (moderator action).
Authorization: Required (Moderator or Admin)
Response (200 OK)
Pin a thread to top of category.
Authorization: Required (Moderator or Admin)
Response (200 OK)
Unpin a thread.
Authorization: Required (Moderator or Admin)
Response (200 OK)
Move thread to different category.
Authorization: Required (Moderator or Admin)
Request:
{
"new_category_id": "uuid",
"reason": "Better suited for this category"
}Response (200 OK):
{
"data": {
"thread": {
"id": "uuid",
"category_id": "uuid",
"updated_at": "2026-01-20T04:00:00Z"
},
"mod_log": {
"action": "thread_move",
"reason": "Better suited for this category"
}
}
}Get post details with edit history and reactions.
Response (200 OK):
{
"data": {
"id": "uuid",
"thread_id": "uuid",
"author": {
"id": "uuid",
"username": "user123"
},
"content": "Post content here",
"content_html": "<p>Post content here</p>",
"reply_to_id": null,
"is_deleted": false,
"edited_at": "2026-01-15T10:00:00Z",
"edit_reason": "Fixed typo",
"reaction_count": 10,
"reactions": [
{ "emoji": "👍", "count": 7, "users": ["user1", "user2"] },
{ "emoji": "❤️", "count": 3, "users": ["user3"] }
],
"edit_history": [
{
"edited_at": "2026-01-15T10:00:00Z",
"editor": "user123",
"reason": "Fixed typo"
}
],
"created_at": "2026-01-10T00:00:00Z"
}
}Create a new post (reply).
Authorization: Required
Request:
{
"thread_id": "uuid",
"content": "My reply to this thread...",
"reply_to_id": "uuid" // Optional, for nested replies
}Response (201 Created):
{
"data": {
"id": "uuid",
"thread_id": "uuid",
"author_id": "uuid",
"content": "My reply to this thread...",
"content_html": "<p>My reply to this thread...</p>",
"reply_to_id": "uuid",
"created_at": "2026-01-20T04:00:00Z"
}
}Edit a post.
Authorization: Required (Author or Moderator)
Request:
{
"content": "Updated post content...",
"edit_reason": "Fixed grammar"
}Response (200 OK):
{
"data": {
"id": "uuid",
"content": "Updated post content...",
"content_html": "<p>Updated post content...</p>",
"edited_at": "2026-01-20T04:00:00Z",
"edit_reason": "Fixed grammar"
}
}Soft-delete a post.
Authorization: Required (Author or Moderator)
Response (204 No Content)
Approve a post in moderation queue.
Authorization: Required (Moderator or Admin)
Response (200 OK):
{
"data": {
"id": "uuid",
"is_approved": true,
"updated_at": "2026-01-20T04:00:00Z"
}
}Reject a post in moderation queue.
Authorization: Required (Moderator or Admin)
Request:
{
"reason": "Spam content"
}Response (200 OK)
Add a reaction to a post.
Authorization: Required
Request:
{
"emoji": "👍"
}Response (200 OK):
{
"data": {
"post_id": "uuid",
"emoji": "👍",
"count": 8
}
}Remove user's reaction from a post.
Authorization: Required
Response (204 No Content)
Get all reactions for a post.
Response (200 OK):
{
"data": [
{
"emoji": "👍",
"count": 7,
"users": [
{ "id": "uuid", "username": "user1" },
{ "id": "uuid", "username": "user2" }
]
},
{
"emoji": "❤️",
"count": 3,
"users": [
{ "id": "uuid", "username": "user3" }
]
}
]
}Full-text search across posts and threads.
Query Parameters:
q(string, required): Search querycategory_id(uuid, optional): Filter by categoryauthor_id(uuid, optional): Filter by authorcreated_after(date, optional): Filter by datelimit(int, default: 20)offset(int, default: 0)
Response (200 OK):
{
"data": [
{
"type": "post",
"post_id": "uuid",
"thread_id": "uuid",
"thread_title": "Welcome to ZapBB!",
"excerpt": "...matching content...",
"author": {
"id": "uuid",
"username": "user123"
},
"category": {
"id": "uuid",
"name": "General Discussion"
},
"relevance_score": 0.95,
"created_at": "2026-01-10T00:00:00Z"
}
],
"pagination": {
"limit": 20,
"offset": 0,
"total": 150
}
}Get moderation action logs.
Authorization: Required (Moderator or Admin)
Query Parameters:
action(string, optional): Filter by action typemoderator_id(uuid, optional): Filter by moderatortarget_type(string, optional): Filter by target typelimit(int, default: 50)offset(int, default: 0)
Response (200 OK):
{
"data": [
{
"id": "uuid",
"moderator": {
"id": "uuid",
"username": "mod_user"
},
"action": "thread_lock",
"target_type": "thread",
"target_id": "uuid",
"reason": "Off-topic discussion",
"metadata": {
"previous_state": { "is_locked": false }
},
"created_at": "2026-01-20T04:00:00Z"
}
],
"pagination": {
"limit": 50,
"offset": 0,
"total": 250
}
}Get user details including warnings and ban status.
Authorization: Required (Moderator or Admin)
Response (200 OK):
{
"data": {
"id": "uuid",
"username": "user123",
"email": "user@example.com",
"role": {
"id": "uuid",
"name": "Member"
},
"is_banned": false,
"warnings": [
{
"id": "uuid",
"reason": "Inappropriate language",
"severity": "warning",
"issued_by": "mod_user",
"issued_at": "2026-01-15T00:00:00Z"
}
],
"ban_history": [],
"post_count": 150,
"thread_count": 25,
"created_at": "2026-01-01T00:00:00Z"
}
}Issue a warning to a user.
Authorization: Required (Moderator or Admin)
Request:
{
"reason": "Inappropriate language",
"severity": "warning",
"expires_at": "2026-02-20T00:00:00Z"
}Response (200 OK):
{
"data": {
"warning": {
"id": "uuid",
"user_id": "uuid",
"reason": "Inappropriate language",
"severity": "warning",
"issued_by_id": "uuid",
"expires_at": "2026-02-20T00:00:00Z",
"issued_at": "2026-01-20T04:00:00Z"
},
"mod_log": {
"id": "uuid",
"action": "user_warn"
}
}
}Ban a user.
Authorization: Required (Moderator or Admin)
Request:
{
"reason": "Repeated spam",
"is_permanent": false,
"banned_until": "2026-02-20T00:00:00Z"
}Response (200 OK):
{
"data": {
"ban": {
"id": "uuid",
"user_id": "uuid",
"reason": "Repeated spam",
"is_permanent": false,
"banned_until": "2026-02-20T00:00:00Z",
"issued_by_id": "uuid",
"created_at": "2026-01-20T04:00:00Z"
},
"mod_log": {
"id": "uuid",
"action": "user_ban"
}
}
}Unban a user.
Authorization: Required (Moderator or Admin)
Response (200 OK)
Get all site settings.
Authorization: Required (Admin only)
Response (200 OK):
{
"data": {
"site_name": "ZapBB Forum",
"site_tagline": "Lightning-fast discussions",
"registration_open": true,
"theme_id": "default",
"moderation_queue_enabled": false,
"smtp_configured": true,
"max_attachment_size": 5242880
}
}Update site settings.
Authorization: Required (Admin only)
Request:
{
"site_name": "My Forum",
"registration_open": false
}Response (200 OK):
{
"data": {
"site_name": "My Forum",
"registration_open": false,
"updated_at": "2026-01-20T04:00:00Z"
}
}All endpoints are rate-limited to prevent abuse:
| Endpoint Type | Rate Limit |
|---|---|
| Authentication | 5 requests/minute |
| Read Operations | 100 requests/minute |
| Write Operations | 30 requests/minute |
| Search | 20 requests/minute |
| Admin Operations | 50 requests/minute |
Rate Limit Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1640000000
| Code | HTTP Status | Description |
|---|---|---|
UNAUTHORIZED |
401 | Not authenticated |
FORBIDDEN |
403 | Not authorized for this action |
NOT_FOUND |
404 | Resource not found |
VALIDATION_ERROR |
422 | Request validation failed |
RATE_LIMIT_EXCEEDED |
429 | Too many requests |
INTERNAL_ERROR |
500 | Server error |
SERVICE_UNAVAILABLE |
503 | Service temporarily unavailable |
Future support for webhooks to notify external systems of events:
thread.createdpost.createduser.registereduser.bannedmoderation.action
- PRD Document: 34c32848.md
- OpenAPI Specification: https://spec.openapis.org/oas/v3.1.0
- REST API Best Practices: https://restfulapi.net/
Document Status: Draft
Next Review: Upon implementation start