Wiki Module
Location: apps/api/src/modules/wiki/
Aggregate roots: WikiEntry, WikiProposal, WikiVote
The Wiki context provides a community-maintained knowledge base for each interest. Proposals start as pending and are approved either automatically when netVotes >= 3 or by an admin override. The rep gate prevents contributions from brand-new accounts.
Constants
| Constant | Value | Description |
|---|---|---|
WIKI_VOTE_APPROVAL_THRESHOLD | 3 | Net votes needed for auto-approval |
WIKI_MIN_REP | 5 | Minimum reputation in the interest to propose or vote |
Prisma Models
| Model | Table | Notes |
|---|---|---|
| WikiEntry | wiki_entries | Canonical versioned content per interest |
| WikiProposal | wiki_proposals | Community-submitted edit |
| WikiVote | wiki_votes | +1 / -1 vote on a proposal |
WikiEntry Fields
| Field | Type | Notes |
|---|---|---|
| id | UUID PK | |
| interestId | UUID FK | One entry per version |
| content | Text | Markdown body |
| summary | String? (280) | |
| authorId | UUID | Admin/approved proposer |
| version | Int | Auto-incremented on approval |
| createdAt | DateTime | |
| updatedAt | DateTime |
Unique: (interestId, version)
WikiProposal Fields
| Field | Type | Notes |
|---|---|---|
| id | UUID PK | |
| interestId | UUID FK | |
| content | Text | |
| summary | String? (280) | |
| proposerId | UUID FK → users | |
| status | String (20) | pending | approved | rejected |
| netVotes | Int | Materialised counter (+1 votes – −1 votes) |
| createdAt | DateTime | |
| updatedAt | DateTime |
WikiVote Fields
| Field | Type | Notes |
|---|---|---|
| id | UUID PK | |
| proposalId | UUID FK | |
| voterId | UUID | |
| value | Int | +1 or -1 |
| createdAt | DateTime |
Unique: (proposalId, voterId) — one vote per user per proposal.
Key Methods
| Method | Description |
|---|---|
getEntry(interestSlug) | Return latest canonical WikiEntry for an interest |
getProposals(interestSlug, opts) | List proposals (all or by status) |
submitProposal(userId, interestSlug, content, summary) | Create pending proposal after rep gate |
vote(proposalId, voterId, value) | Cast or update vote; auto-approves at threshold |
adminApprove(proposalId, adminId) | Force-approve bypassing vote threshold |
adminReject(proposalId, adminId, note) | Reject a proposal |
Rep Gate
checkRepGate(userId, interestId):
- Check
ReputationSnapshotforvalue >= WIKI_MIN_REPin the interest. - Fallback: allow if total reputation across all interests >= 20.
- Otherwise: throw
ForbiddenException.
Auto-Approval Logic
When a vote is cast and proposal.netVotes >= WIKI_VOTE_APPROVAL_THRESHOLD:
- Create a new
WikiEntrywithversion = previous.version + 1. - Update proposal status →
approved. - Emit
wiki.proposal.approved.
HTTP Endpoints
All under /api/v1/wiki/. Require JwtAuthGuard.
| Method | Path | Description |
|---|---|---|
| GET | /wiki/:interestSlug | Get current wiki entry |
| GET | /wiki/:interestSlug/proposals | List proposals |
| POST | /wiki/:interestSlug/proposals | Submit proposal |
| POST | /wiki/proposals/:id/vote | Vote { value: 1 | -1 } |
| POST | /wiki/proposals/:id/approve | Admin approve (AdminGuard) |
| POST | /wiki/proposals/:id/reject | Admin reject (AdminGuard) |
Events Emitted
| Event | Payload | Consumed by |
|---|---|---|
wiki.proposal.approved | { proposalId, interestId, version } | Notifications |