This document defines a production-ready RSVP and registration system for CommDesk events.
It is designed for:
- hackathons
- meetups
- workshops
- conferences
- competitions
The system supports:
- solo registration
- team registration
- identity and contact capture
- configurable form fields
- approval workflow
- secure handling of sensitive data
The RSVP system must:
- collect all required participant details without over-collecting
- support both quick meetup RSVPs and detailed hackathon registrations
- enforce event-specific team rules and capacity limits
- prevent duplicate or fraudulent registrations
- support admin review, approval, rejection, waitlist, and check-in
- provide export and analytics support for operations teams
CommDesk should support two frontend entry modes.
User selects event in form:
Select Event
Event comes from URL:
/events/:eventId/rsvp
Recommendation:
- use event-specific page for public campaigns
- use global page for internal dashboards
Always required:
eventIdregistrationType(SoloorTeam)leader.fullNameleader.emailleader.mobileconsents.eventRulesAcceptedconsents.dataProcessingAccepted
Required only when registrationType = Team:
teamNameteamSizemembers[]
Important identity and contact fields:
fullNameemailmobilecollegeName(optional by event type)collegeId(optional by event type)aadhaarNumber(optional, sensitive)citystatecountry
Strongly recommended additional fields:
dateOfBirthgender(optional enum)githubUrl(optional)linkedinUrl(optional)portfolioUrl(optional)emergencyContactNameemergencyContactMobile
For each member:
fullNameemailmobilecollegeNamecollegeIdcity(optional)state(optional)country(optional)
Must be explicit and versioned:
eventRulesAccepteddataProcessingAcceptedprivacyPolicyVersiontermsVersionmarketingOptIn(optional)
Event admins can define extra questions:
- short text
- long text
- single select
- multi select
- number
- file upload (optional)
- url
Each response should store:
questionIdlabelSnapshotanswer
{
"eventId": "evt_123",
"registrationType": "Solo",
"leader": {
"fullName": "Abhishek Kumar",
"email": "abhishek@example.com",
"mobile": "9876543210",
"collegeName": "CIITM Dhanbad",
"collegeId": "CIITM123",
"aadhaarNumber": "123412341234",
"city": "Dhanbad",
"state": "Jharkhand",
"country": "India"
},
"members": [],
"consents": {
"eventRulesAccepted": true,
"dataProcessingAccepted": true,
"privacyPolicyVersion": "v1.0",
"termsVersion": "v1.0",
"marketingOptIn": false
},
"customResponses": []
}{
"eventId": "evt_123",
"registrationType": "Team",
"teamName": "Code Ninjas",
"teamSize": 4,
"leader": {
"fullName": "Abhishek Kumar",
"email": "abhishek@example.com",
"mobile": "9876543210",
"collegeName": "CIITM Dhanbad",
"collegeId": "CIITM123",
"aadhaarNumber": "123412341234",
"city": "Dhanbad",
"state": "Jharkhand",
"country": "India"
},
"members": [
{
"fullName": "Rahul Sharma",
"email": "rahul@example.com",
"mobile": "9876543211",
"collegeName": "CIITM Dhanbad",
"collegeId": "CIITM124"
},
{
"fullName": "Aman Singh",
"email": "aman@example.com",
"mobile": "9876543212",
"collegeName": "CIITM Dhanbad",
"collegeId": "CIITM125"
},
{
"fullName": "Neha Verma",
"email": "neha@example.com",
"mobile": "9876543213",
"collegeName": "CIITM Dhanbad",
"collegeId": "CIITM126"
}
],
"consents": {
"eventRulesAccepted": true,
"dataProcessingAccepted": true,
"privacyPolicyVersion": "v1.0",
"termsVersion": "v1.0",
"marketingOptIn": false
},
"customResponses": []
}Use two collections:
EventRSVPConfig(event-level rules and form config)EventRegistration(submitted registration records)
import mongoose from "mongoose";
const customQuestionSchema = new mongoose.Schema(
{
questionId: { type: String, required: true },
label: { type: String, required: true },
type: {
type: String,
enum: ["short_text", "long_text", "single_select", "multi_select", "number", "url", "file"],
required: true,
},
required: { type: Boolean, default: false },
options: { type: [String], default: [] },
maxLength: { type: Number },
},
{ _id: false },
);
const EventRSVPConfigSchema = new mongoose.Schema(
{
communityId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
eventId: { type: mongoose.Schema.Types.ObjectId, required: true, unique: true, index: true },
enabled: { type: Boolean, default: true },
startsAt: { type: Date },
endsAt: { type: Date },
allowSolo: { type: Boolean, default: true },
allowTeam: { type: Boolean, default: true },
minTeamSize: { type: Number, default: 2 },
maxTeamSize: { type: Number, default: 4 },
capacityLimit: { type: Number, default: null },
waitlistEnabled: { type: Boolean, default: true },
requireCollegeFields: { type: Boolean, default: false },
requireAadhaar: { type: Boolean, default: false },
customQuestions: { type: [customQuestionSchema], default: [] },
duplicatePolicy: {
blockByEmail: { type: Boolean, default: true },
blockByMobile: { type: Boolean, default: true },
blockByCollegeId: { type: Boolean, default: false },
},
},
{ timestamps: true },
);
export const EventRSVPConfigModel = mongoose.model("EventRSVPConfig", EventRSVPConfigSchema);import mongoose from "mongoose";
const participantSchema = new mongoose.Schema(
{
fullName: { type: String, required: true, trim: true },
email: { type: String, required: true, lowercase: true, trim: true },
mobile: { type: String, required: true, trim: true },
collegeName: { type: String, default: "" },
collegeId: { type: String, default: "" },
city: { type: String, default: "" },
state: { type: String, default: "" },
country: { type: String, default: "" },
},
{ _id: false },
);
const customResponseSchema = new mongoose.Schema(
{
questionId: { type: String, required: true },
labelSnapshot: { type: String, required: true },
answer: { type: mongoose.Schema.Types.Mixed, required: true },
},
{ _id: false },
);
const EventRegistrationSchema = new mongoose.Schema(
{
communityId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true },
eventId: { type: mongoose.Schema.Types.ObjectId, required: true, index: true, ref: "Event" },
registrationNumber: { type: String, required: true, unique: true, index: true },
registrationType: {
type: String,
enum: ["Solo", "Team"],
required: true,
index: true,
},
teamName: { type: String, default: "" },
teamSize: { type: Number, default: 1 },
leader: {
...participantSchema.obj,
aadhaarEncrypted: { type: String, default: "" },
aadhaarLast4: { type: String, default: "" },
emergencyContactName: { type: String, default: "" },
emergencyContactMobile: { type: String, default: "" },
githubUrl: { type: String, default: "" },
linkedinUrl: { type: String, default: "" },
portfolioUrl: { type: String, default: "" },
},
members: { type: [participantSchema], default: [] },
consents: {
eventRulesAccepted: { type: Boolean, required: true },
dataProcessingAccepted: { type: Boolean, required: true },
privacyPolicyVersion: { type: String, required: true },
termsVersion: { type: String, required: true },
marketingOptIn: { type: Boolean, default: false },
},
customResponses: { type: [customResponseSchema], default: [] },
status: {
type: String,
enum: ["Pending", "Approved", "Rejected", "Waitlisted", "Cancelled"],
default: "Pending",
index: true,
},
statusReason: { type: String, default: "" },
checkedIn: { type: Boolean, default: false, index: true },
checkedInAt: { type: Date, default: null },
source: {
channel: { type: String, default: "web" },
ip: { type: String, default: "" },
userAgent: { type: String, default: "" },
},
submittedByUserId: { type: mongoose.Schema.Types.ObjectId, default: null },
},
{ timestamps: true },
);
EventRegistrationSchema.index({ eventId: 1, status: 1, createdAt: -1 });
EventRegistrationSchema.index({ eventId: 1, "leader.email": 1 });
EventRegistrationSchema.index({ eventId: 1, "leader.mobile": 1 });
export const EventRegistrationModel = mongoose.model("EventRegistration", EventRegistrationSchema);Backend must enforce these rules.
eventIdrequiredregistrationTyperequiredleader.fullName,leader.email,leader.mobilerequired- both mandatory consents must be
true
If registrationType = Team:
teamNamerequiredteamSize >= 2teamSize <= config.maxTeamSizemembers.length = teamSize - 1
If registrationType = Solo:
teamNameemptyteamSize = 1members.length = 0
- email format valid
- mobile format valid (E.164 recommended)
- Aadhaar optional unless required by event config
- if Aadhaar provided: exactly 12 digits before encryption
Within same event, block duplicates based on config:
- leader/member email duplicates
- leader/member mobile duplicates
- collegeId duplicates (optional)
Also block duplicates inside same payload:
- no repeated email/mobile among leader + members
- reject submissions if RSVP disabled
- reject submissions before
startsAtor afterendsAt - if capacity reached:
- set
Waitlistedwhen waitlist enabled - else return registration closed error
- set
- required questions must have answers
- response type must match question type
- options must be valid for select-based questions
import { z } from "zod";
const participantSchema = z.object({
fullName: z.string().min(2),
email: z.string().email(),
mobile: z.string().min(8).max(20),
collegeName: z.string().optional(),
collegeId: z.string().optional(),
city: z.string().optional(),
state: z.string().optional(),
country: z.string().optional(),
});
export const createRSVPSchema = z
.object({
eventId: z.string().min(1),
registrationType: z.enum(["Solo", "Team"]),
teamName: z.string().optional(),
teamSize: z.number().int().min(1),
leader: participantSchema.extend({
aadhaarNumber: z
.string()
.regex(/^\d{12}$/)
.optional(),
}),
members: z.array(participantSchema).default([]),
consents: z.object({
eventRulesAccepted: z.literal(true),
dataProcessingAccepted: z.literal(true),
privacyPolicyVersion: z.string().min(1),
termsVersion: z.string().min(1),
marketingOptIn: z.boolean().optional(),
}),
customResponses: z
.array(
z.object({
questionId: z.string().min(1),
answer: z.any(),
}),
)
.default([]),
})
.superRefine((payload, ctx) => {
if (payload.registrationType === "Team") {
if (!payload.teamName || payload.teamName.trim().length < 2) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["teamName"],
message: "teamName is required for team registration",
});
}
if (payload.teamSize < 2) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["teamSize"],
message: "teamSize must be at least 2 for team registration",
});
}
if (payload.members.length !== payload.teamSize - 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["members"],
message: "members length must be teamSize - 1",
});
}
}
if (payload.registrationType === "Solo") {
if (payload.teamSize !== 1) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["teamSize"],
message: "teamSize must be 1 for solo registration",
});
}
if (payload.members.length !== 0) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["members"],
message: "members must be empty for solo registration",
});
}
}
});POST /api/v1/events/:eventId/rsvp
GET /api/v1/events/:eventId/rsvp/:registrationId
PATCH /api/v1/events/:eventId/rsvp/:registrationId
POST /api/v1/events/:eventId/rsvp/:registrationId/cancel
GET /api/v1/events/:eventId/registrations
GET /api/v1/events/:eventId/registrations/:registrationId
PATCH /api/v1/events/:eventId/registrations/:registrationId/approve
PATCH /api/v1/events/:eventId/registrations/:registrationId/reject
PATCH /api/v1/events/:eventId/registrations/:registrationId/waitlist
PATCH /api/v1/events/:eventId/registrations/:registrationId/check-in
GET /api/v1/events/:eventId/registrations/export?format=csv
GET /api/v1/events/:eventId/rsvp-config
PATCH /api/v1/events/:eventId/rsvp-config
Admin list query support:
search
status
registrationType
checkedIn
page
limit
sort
Controller layer:
- parse request
- validate request body and params
- call service layer
- return standardized API response
Service layer:
- load event and RSVP config
- enforce business rules and duplicate policy
- encrypt sensitive fields before save
- assign registration number
- write audit logs
- trigger notifications
Pending -> Approved -> CheckedIn
Pending -> Rejected
Pending -> Waitlisted -> Approved
Approved -> Cancelled
Rules:
- check-in allowed only when status is
Approved - rejected registrations cannot be checked-in
- waitlisted registration can be promoted to approved
Because this system may collect Aadhaar and personal data, apply strict controls.
- never store raw Aadhaar in plaintext
- encrypt at rest (
aadhaarEncrypted) - store only
aadhaarLast4for display - mask Aadhaar in all API responses and exports
- public endpoint only for create and self-view/update (tokenized or authenticated)
- admin endpoints limited to
Owner,Admin,Organizer - enforce community-level authorization on every query
- rate limit RSVP submission endpoint
- CAPTCHA for public forms
- IP and userAgent tracking for abuse detection
- audit logs for status changes and data export
- define retention period per data category
- allow deletion/anonymization workflows when legally required
Recommended checks before save:
- duplicate email/mobile in same event
- duplicate person in same team payload
- team member cannot also be leader in same submission
- block disposable email domains (optional)
- optional OTP verification for mobile/email
Admin should see:
- registration number
- team name
- leader details
- member count and member list
- college and city
- current status
- check-in status
- submission timestamp
Admin actions:
- approve
- reject (with reason)
- waitlist
- check-in
- export CSV
Related system documents:
- CommDesk Participant Platform System
- CommDesk Event System
- CommDesk Judging System
- CommDesk Member Creation and Onboarding System
- Community Signup System
Integration notes:
eventIdandcommunityIdderive from Event System- approved RSVP leader can be promoted to Member through Member System onboarding
- community-level auth policies align with Community Signup and role model
Do not ship RSVP without these fields and controls.
- Event context:
eventIdcommunityId- registration window controls
- Registration mode:
registrationType- team rules (
teamName,teamSize,members)
- Leader identity and contact:
fullNameemailmobile- city/state/country
- Compliance and consent:
- event rules acceptance
- data processing acceptance
- policy version snapshots
- Workflow and operations:
statusstatusReasoncheckedInregistrationNumber
- Security:
- encryption for sensitive identity fields
- rate limiting and abuse checks
- audit logs for admin actions
- Missing consent version capture:
- legal disputes become hard to defend.
- Missing duplicate checks across team and event:
- fake or repeated registrations increase drastically.
- Storing Aadhaar plaintext:
- critical data breach and compliance risk.
- No registration window enforcement:
- users can register after deadline.
- No status reason on reject:
- poor admin traceability and user experience.
- No export audit logs:
- sensitive data can be extracted without trace.
- No index strategy:
- admin list becomes slow for large events.
- No check-in gate:
- unapproved participants can be checked-in.
- No configurable team size:
- hackathon rules become hardcoded and fragile.
- No custom questions support:
- product cannot support diverse event types.
Open RSVP page
-> Event context selected
-> Choose Solo or Team
-> Fill leader details
-> Fill members (if team)
-> Accept terms and data consent
-> Submit RSVP
-> Registration saved (Pending or Waitlisted)
-> Admin reviews and updates status
-> Approved participant checks in on event day
This design gives CommDesk a flexible, secure, and scalable RSVP system with full support for solo and team events.
Delivered capabilities:
- complete identity and contact capture
- team registration logic and validation
- configurable custom questions
- admin workflow and exports
- strict security for sensitive data
- production-ready API and schema design