43 KiB
API Router Reference: Architecture Redesign
Overview
This document provides a complete reference of all tRPC router changes for the MOPC architecture redesign. Every procedure — new, modified, preserved, and removed — is documented with full Zod schemas.
Key Changes Summary:
- Renamed Routers:
pipeline→competition, Stage CRUD split betweencompetitionandroundrouters - New Routers:
jury-group,winner-confirmation,submission-window,mentor-workspace - Enhanced Routers:
assignment,evaluation,file,applicant,award,live-control,mentor - Eliminated:
stageTransitionprocedures (replaced by linear advancement rules) - Procedure Count: ~180 existing procedures, ~60 new, ~30 modified, ~20 removed
Router Rename Map
| Current Router | Redesigned Router | Reason |
|---|---|---|
pipeline.ts |
competition.ts |
Domain-specific naming |
stage.ts |
Split: competition.ts (CRUD) + round.ts (runtime) |
Clearer separation of concerns |
assignment.ts |
assignment.ts (enhanced) |
Add jury group awareness |
specialAward.ts |
award.ts |
Enhanced for two-mode awards |
mentor.ts |
mentor.ts + mentor-workspace.ts |
Separate workspace features |
| (new) | jury-group.ts |
Explicit jury management |
| (new) | winner-confirmation.ts |
Multi-party winner approval |
| (new) | submission-window.ts |
Multi-round document requirements |
1. Competition Router (replaces Pipeline)
File: src/server/routers/competition.ts (renamed from pipeline.ts)
Removed Procedures
All Track-related procedures are eliminated (Track model is removed):
— Replaced bypipeline.createStructurecompetition.createStructure(no tracks)— Replaced bypipeline.getDraftcompetition.getDraft(returns rounds, not tracks)— Replaced bypipeline.updateStructurecompetition.updateStructure(rounds only)— Moved topipeline.getApplicantViewapplicant.getCompetitionView
Modified Procedures
create → competition.create
Changes: Rename references, simplified settings (no track concept)
// Input schema
z.object({
programId: z.string(),
name: z.string().min(1).max(255),
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
// NEW: Competition-wide settings (typed, not generic JSON)
categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'),
startupFinalistCount: z.number().int().min(1).default(3),
conceptFinalistCount: z.number().int().min(1).default(3),
// Notification preferences
notifyOnRoundAdvance: z.boolean().default(true),
notifyOnDeadlineApproach: z.boolean().default(true),
deadlineReminderDays: z.array(z.number().int()).default([7, 3, 1]),
})
// Output: Competition
update → competition.update
Changes: Add typed settings fields
// Input schema
z.object({
id: z.string(),
name: z.string().min(1).max(255).optional(),
slug: z.string().regex(/^[a-z0-9-]+$/).optional(),
status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(),
// NEW: Typed settings
categoryMode: z.enum(['SHARED', 'SPLIT']).optional(),
startupFinalistCount: z.number().int().min(1).optional(),
conceptFinalistCount: z.number().int().min(1).optional(),
notifyOnRoundAdvance: z.boolean().optional(),
notifyOnDeadlineApproach: z.boolean().optional(),
deadlineReminderDays: z.array(z.number().int()).optional(),
})
// Output: Competition
get → competition.get
Changes: Returns rounds array (flat), not tracks
// Input schema
z.object({ id: z.string() })
// Output
{
...competition,
program: { id, name },
rounds: Array<{
id, name, slug, roundType, status, sortOrder,
windowOpenAt, windowCloseAt,
juryGroup: { id, name } | null,
submissionWindow: { id, name } | null,
_count: { projectRoundStates, cohorts }
}>,
juryGroups: Array<{ id, name, _count: { members } }>,
submissionWindows: Array<{ id, name, roundNumber }>,
specialAwards: Array<{ id, name, status }>,
}
list → competition.list
Changes: Returns competition counts (no tracks)
// Input schema
z.object({ programId: z.string() })
// Output
Array<{
...competition,
_count: { rounds, juryGroups, submissionWindows, specialAwards }
}>
publish → competition.publish
Changes: Validates rounds instead of tracks
// Input schema
z.object({ id: z.string() })
// Validation: Must have at least one round, all rounds must have configs
// Output: Competition with status = ACTIVE
New Procedures
createStructure
Purpose: Atomically create Competition + Rounds + Jury Groups + Submission Windows
// Input schema
z.object({
programId: z.string().min(1),
name: z.string().min(1).max(255),
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
// Competition-wide settings
categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'),
startupFinalistCount: z.number().int().default(3),
conceptFinalistCount: z.number().int().default(3),
// Rounds (linear array, no tracks)
rounds: z.array(z.object({
name: z.string().min(1).max(255),
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
roundType: z.enum([
'INTAKE', 'FILTERING', 'EVALUATION',
'SUBMISSION', 'MENTORING', 'LIVE_FINAL', 'CONFIRMATION'
]),
sortOrder: z.number().int().min(0),
configJson: z.record(z.unknown()).optional(),
windowOpenAt: z.date().optional(),
windowCloseAt: z.date().optional(),
// Links to other entities
juryGroupSlug: z.string().optional(), // Will link to JuryGroup after creation
submissionWindowSlug: z.string().optional(),
})),
// Jury groups
juryGroups: z.array(z.object({
name: z.string(),
slug: z.string(),
description: z.string().optional(),
sortOrder: z.number().int(),
defaultMaxAssignments: z.number().int().default(20),
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
softCapBuffer: z.number().int().default(2),
categoryQuotasEnabled: z.boolean().default(false),
defaultCategoryQuotas: z.record(z.object({
min: z.number().int(),
max: z.number().int(),
})).optional(),
})),
// Submission windows
submissionWindows: z.array(z.object({
name: z.string(),
slug: z.string(),
roundNumber: z.number().int(),
sortOrder: z.number().int(),
windowOpenAt: z.date().optional(),
windowCloseAt: z.date().optional(),
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'),
graceHours: z.number().int().optional(),
lockOnClose: z.boolean().default(true),
fileRequirements: z.array(z.object({
name: z.string(),
description: z.string().optional(),
acceptedMimeTypes: z.array(z.string()),
maxSizeMB: z.number().int().optional(),
isRequired: z.boolean().default(true),
sortOrder: z.number().int(),
})),
})),
// Advancement rules (auto-generated linear flow by default)
autoAdvancement: z.boolean().default(true),
})
// Output
{
competition: Competition,
rounds: Array<Round>,
juryGroups: Array<JuryGroup>,
submissionWindows: Array<SubmissionWindow>,
}
updateStructure
Purpose: Diff-based update (create/update/delete rounds, jury groups, windows)
// Input schema
z.object({
id: z.string(),
name: z.string().optional(),
slug: z.string().optional(),
status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(),
// Same nested arrays as createStructure, but with optional `id` fields
rounds: z.array(z.object({
id: z.string().optional(), // Present = update, absent = create
name: z.string(),
// ... all other round fields
})),
juryGroups: z.array(z.object({
id: z.string().optional(),
name: z.string(),
// ... all other jury group fields
})),
submissionWindows: z.array(z.object({
id: z.string().optional(),
name: z.string(),
// ... all other submission window fields
})),
})
// Logic:
// - Rounds/groups/windows with ID → update
// - Rounds/groups/windows without ID → create
// - Existing IDs not in input → delete (with safety checks: no active ProjectRoundStates)
getDraft
Purpose: Get full structure for wizard editing
// Input schema
z.object({ id: z.string() })
// Output
{
...competition,
program: { id, name },
rounds: Array<{
...round,
juryGroup: JuryGroup | null,
submissionWindow: SubmissionWindow | null,
visibleSubmissionWindows: Array<RoundSubmissionVisibility>,
advancementRules: Array<AdvancementRule>,
_count: { projectRoundStates },
}>,
juryGroups: Array<{
...juryGroup,
members: Array<JuryGroupMember>,
_count: { rounds, assignments },
}>,
submissionWindows: Array<{
...submissionWindow,
fileRequirements: Array<SubmissionFileRequirement>,
rounds: Array<{ id, name }>,
}>,
specialAwards: Array<{
...award,
juryGroup: { id, name } | null,
evaluationRound: { id, name } | null,
}>,
}
simulate
Purpose: Dry-run project routing through competition
// Input schema
z.object({
id: z.string(),
projectIds: z.array(z.string()).min(1).max(500),
})
// Output
{
competitionId: string,
competitionName: string,
projectCount: number,
simulations: Array<{
projectId: string,
projectTitle: string,
currentStatus: ProjectStatus,
targetRoundId: string | null,
targetRoundName: string,
}>,
}
Preserved Procedures (renamed only)
delete→competition.delete(archive)getSummary→competition.getSummary(aggregated stats)getStageAnalytics→competition.getRoundAnalytics(per-round stats)
2. Round Router (new, split from Stage)
File: src/server/routers/round.ts
Runtime operations for rounds (open/close windows, transition status, project states).
New Procedures
get
// Input schema
z.object({ id: z.string() })
// Output
{
...round,
competition: { id, name, programId },
juryGroup: JuryGroup | null,
submissionWindow: SubmissionWindow | null,
visibleSubmissionWindows: Array<RoundSubmissionVisibility>,
cohorts: Array<Cohort>,
advancementRules: Array<AdvancementRule>,
stateDistribution: Record<ProjectRoundStateValue, number>,
_count: { projectRoundStates, assignments, evaluationForms },
}
transition
Purpose: State machine for round status
// Input schema
z.object({
id: z.string(),
targetStatus: z.enum(['ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED']),
})
// Validation: Enforces state machine rules
// ROUND_DRAFT → ROUND_ACTIVE
// ROUND_ACTIVE → ROUND_CLOSED
// ROUND_CLOSED → ROUND_ARCHIVED | ROUND_ACTIVE (reopen)
// Output: Round with new status
openWindow
// Input schema
z.object({
id: z.string(),
windowCloseAt: z.date().optional(),
})
// Sets windowOpenAt = now, optionally sets windowCloseAt
// Output: Round
closeWindow
// Input schema
z.object({ id: z.string() })
// Sets windowCloseAt = now
// Output: Round
getProjectStates
Purpose: Paginated list of project states in a round
// Input schema
z.object({
roundId: z.string(),
state: z.enum([
'PENDING', 'IN_PROGRESS', 'PASSED',
'REJECTED', 'COMPLETED', 'WITHDRAWN'
]).optional(),
cursor: z.string().optional(),
limit: z.number().int().min(1).max(100).default(50),
})
// Output
{
items: Array<{
...projectRoundState,
project: { id, title, status, tags, teamName, competitionCategory },
}>,
nextCursor: string | undefined,
}
getForJury
Purpose: Jury-facing round details
// Input schema
z.object({ id: z.string() })
// Output
{
...round,
competition: { id, name, programId },
juryGroup: JuryGroup | null,
evaluationForms: Array<{ id, criteriaJson, scalesJson }>,
isWindowOpen: boolean,
windowTimeRemaining: number | null, // milliseconds
myAssignmentCount: number,
myCompletedCount: number,
}
getApplicantTimeline
Purpose: Show a project's journey through rounds
// Input schema
z.object({
projectId: z.string(),
competitionId: z.string(),
})
// Output
Array<{
roundId: string,
roundName: string,
roundType: RoundType,
state: ProjectRoundStateValue,
enteredAt: DateTime,
exitedAt: DateTime | null,
isCurrent: boolean,
}>
getRequirements
Purpose: File requirements and upload status for a round
// Input schema
z.object({
roundId: z.string(),
projectId: z.string(),
})
// Output
{
submissionWindow: {
id: string,
name: string,
windowOpenAt: DateTime | null,
windowCloseAt: DateTime | null,
deadlinePolicy: DeadlinePolicy,
} | null,
fileRequirements: Array<SubmissionFileRequirement>,
uploadedFiles: Array<ProjectFile>,
windowStatus: {
isOpen: boolean,
closesAt: DateTime | null,
isLate: boolean,
},
deadlineInfo: {
windowOpenAt: DateTime | null,
windowCloseAt: DateTime | null,
graceHours: number,
},
}
3. Jury Group Router (new)
File: src/server/routers/jury-group.ts
New Procedures
create
// Input schema
z.object({
competitionId: z.string(),
name: z.string().min(1).max(255),
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
description: z.string().optional(),
sortOrder: z.number().int().min(0).optional(),
// Default assignment configuration
defaultMaxAssignments: z.number().int().min(1).default(20),
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
softCapBuffer: z.number().int().min(0).default(2),
// Category quotas
categoryQuotasEnabled: z.boolean().default(false),
defaultCategoryQuotas: z.record(z.object({
min: z.number().int().min(0),
max: z.number().int().min(1),
})).optional(),
// Onboarding settings
allowJurorCapAdjustment: z.boolean().default(false),
allowJurorRatioAdjustment: z.boolean().default(false),
})
// Output: JuryGroup
update
// Input schema
z.object({
id: z.string(),
name: z.string().optional(),
description: z.string().optional(),
sortOrder: z.number().int().optional(),
defaultMaxAssignments: z.number().int().optional(),
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
softCapBuffer: z.number().int().optional(),
categoryQuotasEnabled: z.boolean().optional(),
defaultCategoryQuotas: z.record(z.object({
min: z.number().int(),
max: z.number().int(),
})).optional(),
allowJurorCapAdjustment: z.boolean().optional(),
allowJurorRatioAdjustment: z.boolean().optional(),
})
// Output: JuryGroup
delete
// Input schema
z.object({ id: z.string() })
// Validation: Cannot delete if linked to any rounds or assignments
// Output: { success: true }
get
// Input schema
z.object({ id: z.string() })
// Output
{
...juryGroup,
competition: { id, name },
members: Array<{
...juryGroupMember,
user: { id, name, email, expertiseTags },
}>,
rounds: Array<{ id, name, roundType }>,
_count: { members, assignments },
}
list
// Input schema
z.object({ competitionId: z.string() })
// Output
Array<{
...juryGroup,
_count: { members, rounds, assignments },
}>
addMember
// Input schema
z.object({
juryGroupId: z.string(),
userId: z.string(),
isLead: z.boolean().default(false),
// Per-juror overrides
maxAssignmentsOverride: z.number().int().optional(),
capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
categoryQuotasOverride: z.record(z.object({
min: z.number().int(),
max: z.number().int(),
})).optional(),
})
// Validation: User must have role JURY_MEMBER
// Output: JuryGroupMember
updateMember
// Input schema
z.object({
id: z.string(), // JuryGroupMember ID
isLead: z.boolean().optional(),
maxAssignmentsOverride: z.number().int().optional().nullable(),
capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional().nullable(),
categoryQuotasOverride: z.record(z.object({
min: z.number().int(),
max: z.number().int(),
})).optional().nullable(),
// Juror preferences (self-service during onboarding)
preferredStartupRatio: z.number().min(0).max(1).optional(),
availabilityNotes: z.string().optional(),
})
// Output: JuryGroupMember
removeMember
// Input schema
z.object({ id: z.string() })
// Validation: Cannot remove if has active assignments
// Output: { success: true }
bulkAddMembers
// Input schema
z.object({
juryGroupId: z.string(),
userIds: z.array(z.string()).min(1).max(100),
})
// Creates JuryGroupMember for each user with default settings
// Output: { created: number, skipped: number }
getOnboarding
Purpose: Jury member views their onboarding settings for a group
// Input schema (protectedProcedure)
z.object({ juryGroupId: z.string() })
// Output
{
juryGroup: JuryGroup,
membership: JuryGroupMember,
allowCapAdjustment: boolean,
allowRatioAdjustment: boolean,
hasCompleted: boolean, // Has filled preferences
}
submitOnboarding
Purpose: Jury member submits preferences
// Input schema (protectedProcedure)
z.object({
juryGroupId: z.string(),
// Conditionally allowed based on juryGroup.allowJurorCapAdjustment
maxAssignmentsOverride: z.number().int().optional(),
// Conditionally allowed based on juryGroup.allowJurorRatioAdjustment
preferredStartupRatio: z.number().min(0).max(1).optional(),
availabilityNotes: z.string().optional(),
})
// Output: JuryGroupMember
4. Submission Window Router (new)
File: src/server/routers/submission-window.ts
New Procedures
create
// Input schema
z.object({
competitionId: z.string(),
name: z.string().min(1),
slug: z.string().regex(/^[a-z0-9-]+$/),
roundNumber: z.number().int().min(1),
sortOrder: z.number().int().optional(),
// Window timing
windowOpenAt: z.date().optional(),
windowCloseAt: z.date().optional(),
// Deadline behavior
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'),
graceHours: z.number().int().min(0).optional(),
lockOnClose: z.boolean().default(true),
})
// Output: SubmissionWindow
update
// Input schema
z.object({
id: z.string(),
name: z.string().optional(),
windowOpenAt: z.date().optional().nullable(),
windowCloseAt: z.date().optional().nullable(),
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).optional(),
graceHours: z.number().int().optional(),
lockOnClose: z.boolean().optional(),
})
// Output: SubmissionWindow
delete
// Input schema
z.object({ id: z.string() })
// Validation: Cannot delete if has uploaded files
// Output: { success: true }
get
// Input schema
z.object({ id: z.string() })
// Output
{
...submissionWindow,
competition: { id, name },
fileRequirements: Array<SubmissionFileRequirement>,
rounds: Array<{ id, name, roundType }>,
_count: { fileRequirements, projectFiles },
}
list
// Input schema
z.object({ competitionId: z.string() })
// Output
Array<{
...submissionWindow,
_count: { fileRequirements, projectFiles, rounds },
}>
addFileRequirement
// Input schema
z.object({
submissionWindowId: z.string(),
name: z.string().min(1),
description: z.string().optional(),
acceptedMimeTypes: z.array(z.string()).min(1),
maxSizeMB: z.number().int().min(1).optional(),
isRequired: z.boolean().default(true),
sortOrder: z.number().int().optional(),
})
// Output: SubmissionFileRequirement
updateFileRequirement
// Input schema
z.object({
id: z.string(),
name: z.string().optional(),
description: z.string().optional(),
acceptedMimeTypes: z.array(z.string()).optional(),
maxSizeMB: z.number().int().optional(),
isRequired: z.boolean().optional(),
sortOrder: z.number().int().optional(),
})
// Output: SubmissionFileRequirement
deleteFileRequirement
// Input schema
z.object({ id: z.string() })
// Validation: Cannot delete if has uploaded files against this requirement
// Output: { success: true }
setRoundVisibility
Purpose: Configure which submission windows a jury round can see
// Input schema
z.object({
roundId: z.string(),
submissionWindowIds: z.array(z.string()), // Windows jury can see
displayLabels: z.record(z.string()).optional(), // submissionWindowId → label
})
// Creates/updates RoundSubmissionVisibility records
// Output: Array<RoundSubmissionVisibility>
5. Winner Confirmation Router (new)
File: src/server/routers/winner-confirmation.ts
New Procedures
createProposal
Purpose: Admin or system generates a winner proposal
// Input schema
z.object({
competitionId: z.string(),
category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']),
// Proposed rankings (ordered project IDs)
rankedProjectIds: z.array(z.string()).min(1).max(10),
// Evidence/basis
sourceRoundId: z.string(),
selectionBasis: z.object({
method: z.enum(['SCORE_BASED', 'VOTE_BASED', 'AI_RECOMMENDED', 'ADMIN_MANUAL']),
scores: z.record(z.number()).optional(),
aiRecommendation: z.string().optional(),
reasoning: z.string().optional(),
}),
})
// Output: WinnerProposal (status: PENDING)
getProposal
// Input schema
z.object({ id: z.string() })
// Output
{
...winnerProposal,
competition: { id, name },
sourceRound: { id, name, roundType },
proposedBy: { id, name, email },
frozenBy: { id, name } | null,
overrideBy: { id, name } | null,
approvals: Array<{
...winnerApproval,
user: { id, name, email },
}>,
projects: Array<{ // Ordered by rankedProjectIds
id, title, teamName, rank,
}>,
}
listProposals
// Input schema
z.object({
competitionId: z.string(),
status: z.enum([
'PENDING', 'APPROVED', 'REJECTED', 'OVERRIDDEN', 'FROZEN'
]).optional(),
})
// Output
Array<{
...winnerProposal,
approvalProgress: {
total: number,
approved: number,
rejected: number,
pending: number,
},
}>
requestApproval
Purpose: Admin adds jury members to approval list
// Input schema
z.object({
winnerProposalId: z.string(),
userIds: z.array(z.string()).min(1), // Jury members to request approval from
role: z.enum(['JURY_MEMBER', 'ADMIN']).default('JURY_MEMBER'),
})
// Creates WinnerApproval records (approved: null)
// Output: Array<WinnerApproval>
approve
Purpose: Jury member or admin approves the proposal
// Input schema (protectedProcedure)
z.object({
winnerProposalId: z.string(),
approved: z.boolean(),
comments: z.string().optional(),
})
// Updates WinnerApproval.approved = true/false, respondedAt = now
// If all approvals received and all true → sets WinnerProposal.status = APPROVED
// If any approval is false → sets WinnerProposal.status = REJECTED
// Output: WinnerApproval
getMyPendingApprovals
Purpose: Jury member sees proposals awaiting their approval
// Input schema (protectedProcedure)
z.object({ competitionId: z.string().optional() })
// Output
Array<{
...winnerApproval,
proposal: {
...winnerProposal,
competition: { id, name },
projects: Array<{ id, title, rank }>,
},
}>
override
Purpose: Admin forces a result (bypasses jury approval)
// Input schema
z.object({
winnerProposalId: z.string(),
mode: z.enum(['FORCE_MAJORITY', 'ADMIN_DECISION']),
reason: z.string().min(10),
})
// Sets WinnerProposal.status = OVERRIDDEN
// Sets overrideUsed = true, overrideMode, overrideReason, overrideById
// Output: WinnerProposal
freeze
Purpose: Lock the results (official, no further changes)
// Input schema
z.object({ winnerProposalId: z.string() })
// Validation: Must be status APPROVED or OVERRIDDEN
// Sets WinnerProposal.status = FROZEN, frozenAt = now, frozenById
// Updates Competition with winnerProjectIds
// Output: WinnerProposal
unfreeze
Purpose: Admin unlocks results (rare, exceptional circumstances)
// Input schema
z.object({
winnerProposalId: z.string(),
reason: z.string().min(10),
})
// Sets status = PENDING, frozenAt = null
// Creates audit log with reason
// Output: WinnerProposal
6. Assignment Router (enhanced)
File: src/server/routers/assignment.ts
Modified Procedures
create → Enhanced with jury group awareness
Changes: Add juryGroupId parameter, enforce per-juror caps from JuryGroupMember
// Input schema
z.object({
userId: z.string(),
projectId: z.string(),
roundId: z.string(), // RENAMED from stageId
juryGroupId: z.string().optional(), // NEW: Link to jury group
isRequired: z.boolean().default(true),
forceOverride: z.boolean().default(false),
})
// Validation:
// - Checks JuryGroupMember.maxAssignmentsOverride if set
// - Falls back to JuryGroup.defaultMaxAssignments
// - Falls back to User.maxAssignments
// - Enforces CapMode (HARD vs SOFT)
// Output: Assignment (with juryGroupId)
bulkCreate → Enhanced with capacity checking
Changes: Respect juryGroupId, apply category quotas
// Input schema
z.object({
roundId: z.string(), // RENAMED from stageId
juryGroupId: z.string().optional(), // NEW
assignments: z.array(z.object({
userId: z.string(),
projectId: z.string(),
})),
})
// Logic:
// - For each juror, checks current count vs cap
// - If CapMode = HARD → rejects over-cap assignments
// - If CapMode = SOFT → allows up to softCapBuffer extra
// - Applies category quotas if categoryQuotasEnabled
// - Skips assignments that would violate quotas
// Output
{
created: number,
requested: number,
skipped: number,
skippedDueToCapacity: number,
skippedDueToCategoryQuota: number,
}
getSuggestions → Enhanced with category quotas
Changes: Add quota scoring, penalty for over-quota categories
// Input schema
z.object({
roundId: z.string(), // RENAMED from stageId
juryGroupId: z.string().optional(), // NEW
})
// Algorithm enhancements:
// - Fetches JuryGroup.defaultCategoryQuotas
// - Tracks per-juror category distribution
// - Penalizes assignments to over-quota categories (-25 score)
// - Bonuses assignments to under-quota categories (+10 score)
// Output: Array of suggestions with quota awareness
New Procedures
getJuryGroupStats
Purpose: Per-group assignment coverage stats
// Input schema
z.object({
juryGroupId: z.string(),
roundId: z.string(),
})
// Output
{
juryGroup: { id, name },
round: { id, name },
members: Array<{
user: { id, name, email },
currentAssignments: number,
cap: number,
capMode: CapMode,
categoryDistribution: Record<string, number>, // Category → count
quotaStatus: Record<string, { current, min, max, status }>,
}>,
totalAssignments: number,
averageLoad: number,
minLoad: number,
maxLoad: number,
}
7. Evaluation Router (enhanced)
File: src/server/routers/evaluation.ts
Modified Procedures
All procedures: Rename stageId → roundId in inputs/outputs
get, start, autosave, submit — No functional changes, just renames
New Procedures
getCrossRoundDocs
Purpose: Jury sees documents from multiple submission windows
// Input schema (protectedProcedure)
z.object({
projectId: z.string(),
roundId: z.string(),
})
// Logic:
// - Fetches RoundSubmissionVisibility records for roundId
// - Returns files from all visible submission windows
// Output
{
round: { id, name },
submissionWindows: Array<{
window: { id, name, roundNumber },
displayLabel: string,
files: Array<ProjectFile>,
requirements: Array<SubmissionFileRequirement>,
}>,
}
8. File Router (enhanced)
File: src/server/routers/file.ts
Modified Procedures
getDownloadUrl → Enhanced for multi-round visibility
Changes: Check RoundSubmissionVisibility for jury access
// Input schema (no changes)
z.object({
bucket: z.string(),
objectKey: z.string(),
})
// Logic changes:
// - If user is jury, checks Assignment.roundId
// - Looks up RoundSubmissionVisibility for that round
// - Grants access if file.submissionWindowId is in visible windows
// Output: { url }
getUploadUrl → Enhanced for submission window association
Changes: Link file to submissionWindowId
// Input schema
z.object({
projectId: z.string(),
fileName: z.string(),
fileType: z.enum(['EXEC_SUMMARY', 'PRESENTATION', 'VIDEO', 'OTHER']),
mimeType: z.string(),
size: z.number().int().positive(),
submissionWindowId: z.string().optional(), // NEW: Which window is this for?
requirementId: z.string().optional(), // Links to SubmissionFileRequirement
})
// Creates ProjectFile with submissionWindowId
// Output: { uploadUrl, fileId }
New Procedures
promoteFromMentor
Purpose: Promote a MentorFile to official ProjectFile
// Input schema
z.object({
mentorFileId: z.string(),
submissionWindowId: z.string(),
requirementId: z.string().optional(),
})
// Logic:
// - Copies object in MinIO to new key
// - Creates ProjectFile linked to submissionWindow
// - Sets MentorFile.isPromoted = true, promotedToFileId
// Output: ProjectFile
9. Applicant Router (enhanced)
File: src/server/routers/applicant.ts
Modified Procedures
getMySubmission → Enhanced for multi-round
Changes: Returns submission status for all rounds
// Input schema
z.object({
competitionId: z.string().optional(), // NEW: Filter by competition
programId: z.string().optional(),
})
// Output
{
...project,
roundStates: Array<{
round: { id, name, roundType, sortOrder },
state: ProjectRoundStateValue,
enteredAt: DateTime,
exitedAt: DateTime | null,
}>,
currentRound: { id, name, roundType } | null,
eligibleForNextSubmission: boolean,
nextSubmissionWindow: SubmissionWindow | null,
}
New Procedures
getCompetitionView
Purpose: Applicant's view of their journey through a competition
// Input schema (protectedProcedure)
z.object({
competitionId: z.string(),
projectId: z.string(),
})
// Output
{
competition: { id, name, status },
project: { id, title, teamName, status },
rounds: Array<{
round: { id, name, roundType, sortOrder, windowOpenAt, windowCloseAt },
projectState: ProjectRoundState | null,
isCurrent: boolean,
isPassed: boolean,
canSubmit: boolean,
submissionWindow: SubmissionWindow | null,
}>,
timeline: Array<{
date: DateTime,
event: string,
description: string,
}>,
}
submitToWindow
Purpose: Submit files for a specific submission window
// Input schema (protectedProcedure)
z.object({
projectId: z.string(),
submissionWindowId: z.string(),
fileIds: z.array(z.string()), // ProjectFile IDs to mark as submitted
})
// Validation:
// - Window must be open
// - All required FileRequirements must have files
// - User must own project or be team member
// Output: { success: true, submittedAt: DateTime }
10. Live Control Router (enhanced)
File: src/server/routers/live-control.ts (or live.ts)
Modified Procedures
All procedures: Rename stageId → roundId
New Procedures
getStageManager
Purpose: Admin view of live ceremony control panel
// Input schema
z.object({ roundId: z.string() })
// Output
{
round: { id, name, roundType },
liveCursor: LiveProgressCursor | null,
liveVotingSession: LiveVotingSession | null,
// Cohorts (project presentation order)
cohorts: Array<{
...cohort,
projects: Array<{
project: { id, title, teamName },
presentationOrder: number,
}>,
}>,
// Jury voting status
juryVotes: Array<{
juror: { id, name },
votedProjects: Array<string>, // Project IDs
hasCompleted: boolean,
}>,
// Audience voting status
audienceVotes: {
total: number,
byProject: Record<string, number>,
},
// Controls
canStartVoting: boolean,
canCloseVoting: boolean,
canRevealResults: boolean,
}
setCursor
Purpose: Move live ceremony to a specific project/cohort
// Input schema
z.object({
roundId: z.string(),
cohortId: z.string().optional(),
projectId: z.string().optional(),
})
// Updates LiveProgressCursor.currentCohortId, currentProjectId
// Output: LiveProgressCursor
11. Mentor Workspace Router (new)
File: src/server/routers/mentor-workspace.ts
New Procedures
getWorkspace
Purpose: Mentor or applicant views the shared workspace
// Input schema (protectedProcedure)
z.object({ mentorAssignmentId: z.string() })
// Validation: User must be mentor or project team member
// Output
{
mentorAssignment: {
...assignment,
mentor: { id, name, email },
project: { id, title, teamName },
workspaceEnabled: boolean,
workspaceOpenAt: DateTime | null,
workspaceCloseAt: DateTime | null,
},
files: Array<{
...mentorFile,
uploadedBy: { id, name },
comments: Array<MentorFileComment>,
isPromoted: boolean,
promotedToFile: { id, fileName } | null,
}>,
canUpload: boolean,
canComment: boolean,
canPromote: boolean,
}
uploadFile
// Input schema (protectedProcedure)
z.object({
mentorAssignmentId: z.string(),
fileName: z.string(),
mimeType: z.string(),
size: z.number().int().positive(),
description: z.string().optional(),
})
// Creates MentorFile, returns pre-signed upload URL
// Output: { uploadUrl, fileId }
addComment
// Input schema (protectedProcedure)
z.object({
mentorFileId: z.string(),
content: z.string().min(1).max(2000),
parentCommentId: z.string().optional(), // For threading
})
// Output: MentorFileComment
promoteFile
Purpose: Mentor promotes a workspace file to official submission
// Input schema (mentorProcedure)
z.object({
mentorFileId: z.string(),
submissionWindowId: z.string(),
requirementId: z.string().optional(),
})
// Calls file.promoteFromMentor internally
// Output: ProjectFile
12. Special Award Router (renamed to Award, enhanced)
File: src/server/routers/award.ts (renamed from specialAward.ts)
Modified Procedures
create → Enhanced for two-mode awards
Changes: Add eligibilityMode, juryGroupId, evaluationRoundId
// Input schema
z.object({
competitionId: z.string(), // CHANGED from programId
name: z.string().min(1),
description: z.string().optional(),
criteriaText: z.string().optional(),
// NEW: Award mode
eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).default('STAY_IN_MAIN'),
// Scoring/voting
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']),
maxRankedPicks: z.number().int().optional(),
// Decision
decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).default('JURY_VOTE'),
// NEW: Links to other entities
evaluationRoundId: z.string().optional(), // Which round this award runs alongside
juryGroupId: z.string().optional(), // Dedicated or shared jury group
// AI eligibility
useAiEligibility: z.boolean().default(false),
autoTagRulesJson: z.record(z.unknown()).optional(),
})
// Output: SpecialAward
update → Add new fields
// Input schema (all fields optional)
z.object({
id: z.string(),
name: z.string().optional(),
description: z.string().optional(),
criteriaText: z.string().optional(),
eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).optional(), // NEW
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']).optional(),
maxRankedPicks: z.number().int().optional(),
decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).optional(),
evaluationRoundId: z.string().optional(), // NEW
juryGroupId: z.string().optional(), // NEW
useAiEligibility: z.boolean().optional(),
votingStartAt: z.date().optional(),
votingEndAt: z.date().optional(),
})
// Output: SpecialAward
New Procedures
getEligibilityByMode
Purpose: View eligible projects with mode-specific filtering
// Input schema
z.object({ awardId: z.string() })
// Output
{
award: { ...specialAward, eligibilityMode },
eligibleProjects: Array<{
project: { id, title, teamName, competitionCategory },
eligibility: AwardEligibility,
// For SEPARATE_POOL mode
wasRemovedFromMain: boolean,
// For STAY_IN_MAIN mode
mainRoundStatus: ProjectRoundStateValue,
}>,
}
13. Auth & User Router
File: src/server/routers/user.ts
New Procedures
listJuryMembers
Purpose: Admin lists all jury members for jury group assignment
// Input schema
z.object({
programId: z.string().optional(),
onlyActive: z.boolean().default(true),
})
// Output
Array<{
...user,
juryGroupMemberships: Array<{
juryGroup: { id, name, competition: { id, name } },
isLead: boolean,
}>,
_count: { assignments, juryGroupMemberships },
}>
Procedure Access Matrix
| Router | Procedure | Public | Applicant | Jury | Mentor | Observer | Admin | Super Admin |
|---|---|---|---|---|---|---|---|---|
| competition | create | ✓ | ✓ | |||||
| update | ✓ | ✓ | ||||||
| delete | ✓ | ✓ | ||||||
| get | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||
| list | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ||
| publish | ✓ | ✓ | ||||||
| createStructure | ✓ | ✓ | ||||||
| updateStructure | ✓ | ✓ | ||||||
| getDraft | ✓ | ✓ | ||||||
| simulate | ✓ | ✓ | ||||||
| getRoundAnalytics | ✓ | ✓ | ✓ | |||||
| round | get | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| transition | ✓ | ✓ | ||||||
| openWindow | ✓ | ✓ | ||||||
| closeWindow | ✓ | ✓ | ||||||
| getProjectStates | ✓ | ✓ | ✓ | |||||
| getForJury | ✓ | |||||||
| getApplicantTimeline | ✓ | ✓ | ✓ | ✓ | ✓ | |||
| getRequirements | ✓ | ✓ | ✓ | ✓ | ✓ | |||
| jury-group | create | ✓ | ✓ | |||||
| update | ✓ | ✓ | ||||||
| delete | ✓ | ✓ | ||||||
| get | ✓* | ✓ | ✓ | ✓ | ||||
| list | ✓* | ✓ | ✓ | ✓ | ||||
| addMember | ✓ | ✓ | ||||||
| updateMember | ✓ | ✓ | ||||||
| removeMember | ✓ | ✓ | ||||||
| bulkAddMembers | ✓ | ✓ | ||||||
| getOnboarding | ✓* | |||||||
| submitOnboarding | ✓* | |||||||
| submission-window | create | ✓ | ✓ | |||||
| update | ✓ | ✓ | ||||||
| delete | ✓ | ✓ | ||||||
| get | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | ||
| list | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | ||
| addFileRequirement | ✓ | ✓ | ||||||
| updateFileRequirement | ✓ | ✓ | ||||||
| deleteFileRequirement | ✓ | ✓ | ||||||
| setRoundVisibility | ✓ | ✓ | ||||||
| winner-confirmation | createProposal | ✓ | ✓ | |||||
| getProposal | ✓* | ✓ | ✓ | ✓ | ||||
| listProposals | ✓ | ✓ | ✓ | |||||
| requestApproval | ✓ | ✓ | ||||||
| approve | ✓* | ✓* | ✓ | |||||
| getMyPendingApprovals | ✓* | ✓* | ✓ | |||||
| override | ✓ | ✓ | ||||||
| freeze | ✓ | ✓ | ||||||
| unfreeze | ✓ | |||||||
| assignment | create | ✓ | ✓ | |||||
| bulkCreate | ✓ | ✓ | ||||||
| delete | ✓ | ✓ | ||||||
| listByRound | ✓ | ✓ | ✓ | |||||
| listByProject | ✓ | ✓ | ✓ | |||||
| myAssignments | ✓ | |||||||
| get | ✓* | ✓* | ✓ | ✓ | ||||
| getStats | ✓ | ✓ | ✓ | |||||
| getSuggestions | ✓ | ✓ | ||||||
| getAISuggestions | ✓ | ✓ | ||||||
| applySuggestions | ✓ | ✓ | ||||||
| getJuryGroupStats | ✓ | ✓ | ✓ | |||||
| evaluation | get | ✓* | ✓ | ✓ | ||||
| start | ✓* | |||||||
| autosave | ✓* | |||||||
| submit | ✓* | |||||||
| getProjectStats | ✓ | ✓ | ✓ | |||||
| listByRound | ✓ | ✓ | ✓ | |||||
| myPastEvaluations | ✓ | |||||||
| declareCOI | ✓* | |||||||
| getCOIStatus | ✓* | ✓ | ✓ | |||||
| listCOIByRound | ✓ | ✓ | ||||||
| reviewCOI | ✓ | ✓ | ||||||
| generateSummary | ✓ | ✓ | ||||||
| getSummary | ✓ | ✓ | ✓ | |||||
| getCrossRoundDocs | ✓* | |||||||
| file | getDownloadUrl | ✓* | ✓* | ✓* | ✓ | ✓ | ||
| getUploadUrl | ✓ | ✓ | ||||||
| promoteFromMentor | ✓* | ✓ | ✓ | |||||
| applicant | getSubmissionBySlug | ✓ | ✓ | |||||
| getMySubmission | ✓ | |||||||
| saveSubmission | ✓ | |||||||
| getCompetitionView | ✓* | |||||||
| submitToWindow | ✓* | |||||||
| mentor-workspace | getWorkspace | ✓* | ✓* | ✓ | ✓ | |||
| uploadFile | ✓* | ✓* | ||||||
| addComment | ✓* | ✓* | ||||||
| promoteFile | ✓ | ✓ | ✓ | |||||
| award | create | ✓ | ✓ | |||||
| update | ✓ | ✓ | ||||||
| delete | ✓ | ✓ | ||||||
| get | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | ||
| list | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ | ||
| updateStatus | ✓ | ✓ | ||||||
| runEligibility | ✓ | ✓ | ||||||
| getEligibilityByMode | ✓ | ✓ | ✓ | |||||
| live-control | getStageManager | ✓ | ✓ | |||||
| setCursor | ✓ | ✓ | ||||||
| startVoting | ✓ | ✓ | ||||||
| closeVoting | ✓ | ✓ | ||||||
| castJuryVote | ✓* | |||||||
| castAudienceVote | ✓* | ✓* | ✓* | ✓* | ✓* | ✓* |
Legend:
✓*= Conditional access (must be assigned, own project, be in jury group, etc.)✓= Unconditional access for that role
Migration Checklist: tRPC Updates
Phase 1: Rename Existing Routers
- Rename
pipeline.ts→competition.ts - Update all imports in UI components
- Update all
trpc.pipeline.*calls →trpc.competition.*
Phase 2: Split Stage Router
- Create
round.tsfor runtime operations - Move procedures:
transition,openWindow,closeWindow,getProjectStates,getForJury,getApplicantTimeline,getRequirements - Update all
trpc.stage.*calls to reference correct router
Phase 3: Create New Routers
- Create
jury-group.ts - Create
winner-confirmation.ts - Create
submission-window.ts - Create
mentor-workspace.ts
Phase 4: Enhance Existing Routers
- Update
assignment.tswith jury group awareness - Update
evaluation.tswith cross-round doc visibility - Update
file.tswith submission window association - Update
applicant.tswith multi-round support - Rename
specialAward.ts→award.tsand enhance
Phase 5: Update App Router
- Update
src/server/routers/_app.tswith new routers - Export all renamed routers
- Run type generation:
npm run trpc:generate
Total Procedures:
- Current: ~180
- New: ~60
- Modified: ~30
- Removed: ~20
- Final: ~250
This comprehensive reference ensures every API change is documented with complete type safety.