This document defines the judge-first judging system for CommDesk.
Primary scope:
- judge invitation and onboarding
- judge login and session management
- judge scoring workflow
- score locking and auditability
- optional transparent public publishing
This is designed for events where judges log in, evaluate projects, and submit scores in a controlled, fair, and traceable way.
The judging system must:
- allow only authorized judges to score submissions
- enforce consistent criteria-based scoring
- prevent score tampering after submission
- provide a full audit trail for every judging action
- support transparency mode for public trust
- generate leaderboard automatically from submitted scores
Judge invited
-> Invite accepted
-> Judge logs in
-> Sees assigned submissions
-> Scores criteria and adds feedback
-> Submits final score
-> Score becomes locked
-> Leaderboard recalculates
-> Public view updates (if transparency enabled)
Judge: can score assigned submissionsLeadJudge: can review, unlock with reason, and finalize roundsOrganizer/Admin: can configure criteria, assign judges, and publish visibility
Action Judge LeadJudge Organizer/Admin
View assigned submissions Yes Yes Yes
Save draft score Yes Yes No
Submit final score Yes Yes No
Edit score after submit No Conditional No
Unlock score No Yes Conditional
Create/Update criteria No No Yes
Assign judges No Yes Yes
Toggle transparency settings No No Yes
Finalize judging round No Yes Yes
Organizer invites judge
-> Invite token generated
-> Email sent
-> Judge accepts invite
-> Judge account linked
-> Judge can login
Recommended support:
- email + password login
- magic link login (optional)
- SSO (optional, enterprise)
- short-lived access token + refresh token
- optional MFA for judges
- login rate limiting
- device/session tracking
- forced logout if role revoked
This section includes production-ready schema shapes for judge login and scoring.
EventJudgeAssignment;
{
_id: ObjectId;
eventId: ObjectId;
communityId: ObjectId;
userId: ObjectId;
memberId: ObjectId;
displayProfile: {
name: String;
title: String;
company: String;
bio: String;
avatarUrl: String;
}
expertiseTags: [String];
status: "Invited" | "Active" | "Disabled";
access: {
role: "Judge" | "LeadJudge";
canScore: Boolean;
canFinalize: Boolean;
canUnlockScore: Boolean;
}
transparency: {
judgingVisible: Boolean;
showNamePublic: Boolean;
}
invitedBy: ObjectId;
invitedAt: Date;
acceptedAt: Date;
lastLoginAt: Date;
createdAt: Date;
updatedAt: Date;
}Required indexes:
- unique:
(eventId, userId) - index:
(eventId, status)
EventJudgingCriteria;
{
_id: ObjectId;
eventId: ObjectId;
communityId: ObjectId;
name: String;
description: String;
maxScore: Number;
weight: Number;
required: Boolean;
visibleToPublic: Boolean;
order: Number;
createdBy: ObjectId;
updatedBy: ObjectId;
createdAt: Date;
updatedAt: Date;
}Validation:
maxScore > 0weight > 0- total criteria weight should equal
100(recommended)
EventSubmission
{
_id: ObjectId
eventId: ObjectId
communityId: ObjectId
teamId: ObjectId
projectName: String
slug: String
shortDescription: String
fullDescription: String
category: String
track: String
techStack: [String]
repositoryUrl: String
demoUrl: String
presentationUrl: String
videoUrl: String
screenshots: [String]
status:
"Draft"
| "Submitted"
| "UnderReview"
| "Finalist"
| "Winner"
judgingVisible: Boolean
scoreSummary:
{
totalScore: Number
averageScore: Number
weightedAverageScore: Number
judgeCount: Number
}
publicScoreBreakdown:
[
{
judgeId: ObjectId
judgeName: String
totalScore: Number
weightedScore: Number
}
]
submittedAt: Date
createdAt: Date
updatedAt: Date
}SubmissionScore
{
_id: ObjectId
eventId: ObjectId
communityId: ObjectId
submissionId: ObjectId
judgeId: ObjectId
judgeInfo:
{
name: String
title: String
company: String
}
criteriaScores:
[
{
criteriaId: ObjectId
criteriaName: String
criteriaDescription: String
score: Number
maxScore: Number
weight: Number
weightedScore: Number
}
]
totalScore: Number
weightedScore: Number
feedback:
{
privateNote: String
publicNote: String
}
visibility:
"Private"
| "Public"
status:
"Draft"
| "Submitted"
| "Finalized"
isLocked: Boolean
submittedAt: Date
finalizedAt: Date
scoreVersion: Number
createdAt: Date
updatedAt: Date
}Required indexes:
- unique:
(eventId, submissionId, judgeId) - index:
(eventId, judgeId, status) - index:
(eventId, submissionId)
EventLeaderboardCache;
{
_id: ObjectId;
eventId: ObjectId;
submissionId: ObjectId;
teamId: ObjectId;
rank: Number;
averageScore: Number;
weightedAverageScore: Number;
judgeCount: Number;
tieBreakerMeta: {
highestSingleJudgeScore: Number;
scoreStdDeviation: Number;
earliestSubmittedAt: Date;
}
updatedAt: Date;
}JudgingAudit;
{
_id: ObjectId;
eventId: ObjectId;
communityId: ObjectId;
submissionId: ObjectId;
judgeId: ObjectId;
actorUserId: ObjectId;
action: "InviteSent" |
"InviteAccepted" |
"JudgeLogin" |
"ScoreDraftSaved" |
"ScoreSubmitted" |
"ScoreUnlockRequested" |
"ScoreUnlocked" |
"ScoreFinalized" |
"JudgingRoundFinalized";
metadata: Mixed;
ipAddress: String;
userAgent: String;
createdAt: Date;
}JudgingRound;
{
_id: ObjectId;
eventId: ObjectId;
communityId: ObjectId;
roundNumber: Number;
name: String;
status: "Upcoming" | "Active" | "Completed" | "Cancelled";
scoringDeadline: Date;
startedAt: Date;
completedAt: Date;
assignedSubmissionIds: [ObjectId];
finalizedBy: ObjectId;
finalizedAt: Date;
createdBy: ObjectId;
createdAt: Date;
updatedAt: Date;
}Required indexes:
- unique:
(eventId, roundNumber) - index:
(eventId, status)
Links individual judges to specific submissions within a round. Supports round-robin, manual, and category-based strategies.
JudgeSubmissionAssignment;
{
_id: ObjectId;
eventId: ObjectId;
communityId: ObjectId;
roundId: ObjectId;
judgeId: ObjectId;
submissionId: ObjectId;
assignmentStrategy: "Manual" | "RoundRobin" | "CategoryBased";
status: "Pending" | "InProgress" | "Completed";
assignedBy: ObjectId;
assignedAt: Date;
createdAt: Date;
updatedAt: Date;
}Required indexes:
- unique:
(roundId, judgeId, submissionId) - index:
(eventId, judgeId, status) - index:
(eventId, submissionId)
JudgeConflictOfInterest;
{
_id: ObjectId;
eventId: ObjectId;
communityId: ObjectId;
judgeId: ObjectId;
submissionId: ObjectId;
declaredAt: Date;
reason: String;
resolvedBy: ObjectId;
resolvedAt: Date;
resolution: "Excluded" | "WaivedByOrganizer";
createdAt: Date;
}Required indexes:
- unique:
(eventId, judgeId, submissionId) - index:
(eventId, judgeId)
Store in event-level settings.
judgingSettings;
{
mode: "Private" | "Transparent";
showJudgeNames: Boolean;
showCriteria: Boolean;
showFeedback: Boolean;
publishTiming: "Live" | "AfterRoundComplete" | "AfterEventComplete";
lockEditsAfterSubmit: Boolean;
allowLeadJudgeUnlock: Boolean;
minJudgeCountForLeaderboard: Number;
blindedJudging: Boolean;
scoringDeadline: Date;
assignmentStrategy: "Manual" | "RoundRobin" | "CategoryBased";
}Recommended defaults:
mode = PrivateshowJudgeNames = falseshowFeedback = falsepublishTiming = AfterRoundCompletelockEditsAfterSubmit = trueblindedJudging = falseassignmentStrategy = Manual
Draft -> Submitted -> Finalized
Rules:
Draft: judge can edit anytime before submission deadlineSubmitted: locked for judge editsFinalized: immutable, included in final ranking
- judge cannot edit once status is
Submitted - unlock allowed only for
LeadJudgeorOrganizer/Adminwith reason - each unlock increments
scoreVersion - all unlocks logged in
JudgingAudit
Before score submission:
- all required criteria must be scored
- each criterion score must be within
0..maxScore - judge must be assigned to event and active
- judge must not have conflict of interest for that submission
For each criterion:
criterionNormalized = (score / maxScore) * weight
Judge weighted total:
judgeWeightedScore = sum(criterionNormalized)
averageScore = average(totalScore across submitted judges)
weightedAverageScore = average(weightedScore across submitted judges)
judgeCount = total submitted judges for submission
Primary sorting:
weightedAverageScoredescaverageScoredeschighestSingleJudgeScoredescearliestSubmittedAtasc
Tie-break policy must be documented in event rules.
Route:
/judge/login
Route:
/judge/events/:eventId
Displays:
- assigned submissions
- score status (
NotStarted,Draft,Submitted) - deadline countdown
- filters by track/category
Route:
/judge/events/:eventId/submissions/:submissionId/score
Sections:
- project details
- team details
- criteria scoring form
- private/public feedback blocks
- draft save and final submit actions
Only active when transparency mode allows publishing.
/events/:eventId/judging
Show:
- judge list (if allowed)
- criteria and weights (if allowed)
- project list
- score overview
/events/:eventId/projects/:slug
Show:
- per-judge score cards
- criteria-level breakdown
- public feedback
/events/:eventId/leaderboard
Show:
- rank
- project/team
- final average
- judge count
POST /api/v1/judge/auth/accept-invite
POST /api/v1/judge/auth/login
POST /api/v1/judge/auth/refresh
POST /api/v1/judge/auth/logout
GET /api/v1/judge/events/:eventId/dashboard
GET /api/v1/judge/events/:eventId/submissions
GET /api/v1/judge/events/:eventId/submissions/:submissionId
POST /api/v1/judge/events/:eventId/submissions/:submissionId/scores/draft
POST /api/v1/judge/events/:eventId/submissions/:submissionId/scores/submit
GET /api/v1/judge/events/:eventId/my-scores
POST /api/v1/events/:eventId/judges/invite
GET /api/v1/events/:eventId/judges
PATCH /api/v1/events/:eventId/judges/:judgeId
POST /api/v1/events/:eventId/judges/:judgeId/disable
POST /api/v1/events/:eventId/criteria
GET /api/v1/events/:eventId/criteria
PATCH /api/v1/events/:eventId/criteria/:criteriaId
DELETE /api/v1/events/:eventId/criteria/:criteriaId
PATCH /api/v1/events/:eventId/judging-settings
POST /api/v1/events/:eventId/scores/:scoreId/unlock
POST /api/v1/events/:eventId/judging/finalize-round
GET /api/v1/events/:eventId/judging
GET /api/v1/events/:eventId/leaderboard
GET /api/v1/events/:eventId/projects
GET /api/v1/events/:eventId/projects/:slug
POST /api/v1/events/:eventId/judging/rounds
GET /api/v1/events/:eventId/judging/rounds
GET /api/v1/events/:eventId/judging/rounds/:roundId
PATCH /api/v1/events/:eventId/judging/rounds/:roundId
POST /api/v1/events/:eventId/judging/rounds/:roundId/finalize
POST /api/v1/events/:eventId/judging/rounds/:roundId/assignments
GET /api/v1/events/:eventId/judging/rounds/:roundId/assignments
DELETE /api/v1/events/:eventId/judging/rounds/:roundId/assignments/:assignmentId
POST /api/v1/events/:eventId/judging/rounds/:roundId/assignments/auto-assign
POST /api/v1/judge/events/:eventId/conflicts
GET /api/v1/events/:eventId/judging/conflicts
PATCH /api/v1/events/:eventId/judging/conflicts/:conflictId/resolve
Backend must enforce:
- judge must be
Activeand assigned to event - submission must be in scoreable state (
SubmittedorUnderReview) - no score above criterion
maxScore - score submission requires all required criteria
- one final score per judge per submission version
- cannot score own team/project
- cannot score after round finalization
Required controls:
- immutable score after submit
- unlock only through privileged action with reason
- conflict-of-interest declaration
- all write actions captured in
JudgingAudit - timestamped submissions
- IP and user-agent recording
Recommended controls:
- blinded judging mode (hide team identity)
- anomaly detection for outlier scores
- score normalization policy for judge bias
- cryptographic hash for finalized score payload
If mode = Transparent:
- public pages can show criteria and score breakdown
- judge names shown only when
showJudgeNames = true - public feedback shown only when
showFeedback = true - publishing obeys
publishTiming
If mode = Private:
- no judge score breakdown exposed publicly
- leaderboard can still be internal for organizers
Do not ship without these judge system fields.
- Judge identity and access:
userIdeventIdrolestatus
- Criteria integrity:
criteriaIdcriteriaNamemaxScoreweightorder
- Score traceability:
judgeIdsubmissionIdcriteriaScores[]totalScoreweightedScorestatusisLockedscoreVersion
- Security and audits:
createdAt,submittedAt,finalizedAtipAddress,userAgent- audit
action - unlock reason metadata
- Transparency controls:
modeshowJudgeNamesshowFeedbackpublishTimingblindedJudging
- Round management:
roundNumberscoringDeadlinestatusfinalizedAt
- Assignment tracking:
JudgeSubmissionAssignmentrecordsassignmentStrategy
- Conflict of interest:
JudgeConflictOfInterestrecordsdeclaredAtreasonresolution
- No score locking:
- judges can alter historical results.
- No audit trail:
- disputes cannot be resolved with evidence.
- Missing criteria snapshots in score records:
- criteria renames break historical transparency.
- No conflict-of-interest enforcement:
- trust in judging collapses.
- No round finalization:
- leaderboard can keep changing after winners announced.
- No tie-break policy:
- ranking disputes increase.
- No transparency timing controls:
- sensitive feedback may leak too early.
- Missing unique score constraint:
- duplicate scoring by same judge corrupts averages.
- No role revocation handling:
- disabled judges may still access scoring APIs.
- No visibility toggles:
- private events may expose judge details unintentionally.
- No judging round model:
- no deadline enforcement, round sequencing, or controlled finalization per round.
- No judge-to-submission assignment records:
- impossible to trace which judge was responsible for which submission or enforce assignment limits.
- Criteria description not snapshotted:
- editing criteria description after scoring creates silent inconsistencies in historical records.
- No blinded judging toggle:
- team identity can bias scores even when blinded mode is desired.
Related documents:
- CommDesk Participant Platform System
- CommDesk Sponsor & Partner Management System
- CommDesk Event System
- CommDesk RSVP System
- CommDesk Member Creation & Onboarding System
Integration notes:
- event metadata, status, and settings come from Event System
- sponsor challenge judging and sponsor prize visibility can be extended through the Sponsor and Partner System
- judge identity and account linkage come from Member/User model
- team and submission sources integrate with event participation flows
Event
-> EventJudgingCriteria
-> EventJudgeAssignment
-> JudgingRound
-> JudgeSubmissionAssignment
-> EventSubmission
-> SubmissionScore
-> JudgingAudit
-> JudgeConflictOfInterest
-> LeaderboardCache (optional)
-> Public Judging Pages (when enabled)
This design gives CommDesk a robust judge login and scoring system with optional transparency.
Delivered capabilities:
- secure judge onboarding and authentication
- structured criteria scoring
- immutable submission workflow
- leaderboard computation and caching
- transparency controls per event
- audit-ready anti-manipulation model
Defines when the system must dispatch notifications (email, in-app, or push).
| Event | Recipients | Trigger | Channel |
|---|---|---|---|
| Judge invited | Judge | Organizer sends invite | |
| Invite about to expire | Judge | 24h before invite token expiry | |
| Invite accepted | Organizer | Judge accepts invite | In-app |
| Scoring round opened | All assigned judges | Round status → Active | Email + In-app |
| Scoring deadline reminder | Judges with pending scores | 48h and 24h before scoringDeadline |
|
| Score submitted | Lead Judge | Judge submits final score | In-app |
| Score unlocked | Judge | Lead Judge/Admin unlocks their score | Email + In-app |
| Round finalized | All judges, Organizer | Round status → Completed | Email + In-app |
| Conflict of interest raised | Organizer, Lead Judge | Judge declares conflict | In-app |
| Conflict resolved | Judge | Organizer resolves conflict | In-app |
| Leaderboard published | Participants (if public) | Transparency publish trigger | Email + In-app |
All notification sends must be logged for audit purposes.
All judging API endpoints must return consistent error shapes.
ErrorResponse;
{
status: Number; // HTTP status code
code: String; // machine-readable error code
message: String; // human-readable description
field?: String; // present on validation errors
meta?: Mixed; // optional debug context (dev only)
}| HTTP | Code | Meaning |
|---|---|---|
| 400 | VALIDATION_ERROR |
Missing or invalid field |
| 400 | CRITERIA_SCORE_OUT_OF_RANGE |
Score exceeds maxScore |
| 400 | REQUIRED_CRITERIA_MISSING |
Not all required criteria scored |
| 401 | UNAUTHORIZED |
Not authenticated |
| 403 | FORBIDDEN |
Authenticated but lacks permission |
| 403 | JUDGE_NOT_ASSIGNED |
Judge not assigned to this submission |
| 403 | CONFLICT_OF_INTEREST |
Judge declared a conflict for this submission |
| 403 | SCORE_LOCKED |
Score is locked and cannot be edited |
| 403 | ROUND_FINALIZED |
Round is closed, no further scoring allowed |
| 404 | NOT_FOUND |
Resource does not exist |
| 409 | DUPLICATE_SCORE |
Submitted score already exists for this judge+submission |
| 409 | INVITE_ALREADY_ACCEPTED |
Invite token already consumed |
| 410 | INVITE_EXPIRED |
Invite token has expired |
| 422 | SCORING_DEADLINE_PASSED |
Submission attempted after round deadline |