| name | mongodb-event-platform |
| description | Build event/hackathon management with lifecycle states, multi-step registration, dynamic feedback forms, weighted judging rubrics, and landing pages with slug-based routing |
| license | MIT |
| metadata | {"version":"1.0.0","author":"Michael Lynn [mlynn.org](https://mlynn.org)","category":"mongodb-devrel","domain":"event-management","updated":"2026-03-01T00:00:00.000Z","python-tools":"feedback_form_generator.py","tech-stack":"nextjs, mongoose, zod, geospatial"} |
mongodb-event-platform
Trigger
Use this skill when building event/hackathon/workshop/conference management features: event CRUD with lifecycle states, multi-step registration with capacity management, dynamic feedback forms, judging rubrics with weighted scoring, and landing pages with slug-based routing.
This is the big one. Lifecycle states, registration flows, judging rubrics, feedback forms — basically everything I learned running hackathons for 3 years, codified. — ML
Overview
MongoDB DevRel runs hackathons, .local conferences, community days, workshops, and meetups. This skill covers them all with a flexible Event model that supports the full lifecycle from draft to conclusion, customizable registration flows, dynamic feedback collection, judging with weighted rubrics, and a landing page builder with content sections.
How to Use
Quick Start
Invoke with /mongodb-event-platform or let Claude auto-activate when building event management features.
Python Tools
scripts/feedback_form_generator.py — Generate FeedbackFormConfig JSON from a simple YAML spec
Reference Docs
references/event-lifecycle.md — State machine: draft → open → in_progress → concluded
references/registration-flow.md — 3-tier registration wizard flow + capacity checks
Templates & Samples
assets/sample_event.json — Example Event document with all fields
assets/sample_feedback_form.json — Example FeedbackFormConfig
assets/sample_rubric.json — Example judging rubric with weighted criteria
Architecture Decisions
- Lifecycle states (draft → open → in_progress → concluded): Events flow through defined states. Registration is only allowed when status is "open". Results are published when status is "concluded".
- 3-tier registration wizard: Tier 1 is always shown (email, password, terms). Tier 2 is configurable (skills, github, bio). Tier 3 is fully custom questions per event. This balances simplicity for casual events with depth for competitive hackathons.
- Session-persisted wizard progress: Registration progress is stored in
sessionStorage so users can navigate away and return without losing their input.
- Dynamic feedback forms with 6 question types: Forms are configured as JSON documents (sections → questions), not hardcoded. Admins can create forms with any combination of short_text, long_text, multiple_choice, checkbox, linear_scale, and rating questions.
- Weighted judging rubrics: Each rubric criterion has a weight and maxScore, enabling normalized scoring across different evaluation dimensions.
- Landing page slug routing: Events can have public landing pages at
/{slug} with customizable hero, about, prizes, schedule, sponsors, and FAQ sections.
- Geospatial indexing: Events store coordinates with a 2dsphere index for map-based discovery.
Code Patterns
Pattern 1: Event Model
import mongoose, { Schema, Document, Types } from "mongoose";
export interface IEvent extends Document {
name: string;
description: string;
theme: string;
startDate: Date;
endDate: Date;
registrationDeadline: Date;
submissionDeadline?: Date;
location: string;
city?: string;
country?: string;
coordinates?: { type: "Point"; coordinates: [number, number] };
venue?: string;
capacity: number;
isVirtual: boolean;
tags: string[];
rules: string;
judging_criteria: string[];
judgingRubric?: { name: string; description: string; weight: number; maxScore: number }[];
organizers: Types.ObjectId[];
partners: Types.ObjectId[];
status: "draft" | "open" | "in_progress" | "concluded";
resultsPublished: boolean;
resultsPublishedAt?: Date;
feedbackForms?: { participant?: Types.ObjectId; partner?: Types.ObjectId };
atlasProvisioning?: {
enabled: boolean;
defaultProvider: 'AWS' | 'GCP' | 'AZURE';
defaultRegion: string;
openNetworkAccess: boolean;
maxDbUsersPerCluster: number;
autoCleanupOnEventEnd: boolean;
};
descriptionEmbedding?: number[];
landingPage?: {
template: string;
slug: string;
published: boolean;
registrationFormConfig?: Types.ObjectId;
customContent: {
hero?: { headline?: string; subheadline?: string; ctaText?: string; backgroundImage?: string };
about?: string;
prizes?: Array<{ title: string; description: string; value?: string }>;
schedule?: Array<{ time: string; title: string; description?: string }>;
sponsors?: Array<{ name: string; logo: string; tier: string }>;
faq?: Array<{ question: string; answer: string }>;
};
};
}
Pattern 2: Dynamic Feedback Form Config
export interface IScaleConfig {
min: number; max: number; minLabel: string; maxLabel: string;
}
export interface IFeedbackQuestion {
id: string;
type: "short_text" | "long_text" | "multiple_choice" | "checkbox" | "linear_scale" | "rating";
label: string;
description: string;
required: boolean;
placeholder: string;
options: string[];
scaleConfig?: IScaleConfig;
}
export interface IFeedbackSection {
id: string;
title: string;
description: string;
questions: IFeedbackQuestion[];
}
export interface IFeedbackFormConfig extends Document {
name: string;
slug: string;
description: string;
isBuiltIn: boolean;
createdBy?: Types.ObjectId;
targetAudience: "participant" | "partner" | "both";
sections: IFeedbackSection[];
}
Pattern 3: 3-Tier Registration Form Config
interface ICustomQuestion {
id: string;
label: string;
type: "text" | "select" | "multiselect" | "checkbox";
options: string[];
required: boolean;
placeholder: string;
}
export interface IRegistrationFormConfig extends Document {
name: string;
slug: string;
description: string;
isBuiltIn: boolean;
tier1: {
showExperienceLevel: boolean;
customQuestions: ICustomQuestion[];
};
tier2: {
enabled: boolean;
prompt: string;
showSkills: boolean;
showGithub: boolean;
showBio: boolean;
customQuestions: ICustomQuestion[];
};
tier3: {
enabled: boolean;
prompt: string;
customQuestions: ICustomQuestion[];
};
}
Pattern 4: Event CRUD API Route
import { NextRequest } from "next/server";
import { auth } from "@/lib/auth";
import { connectToDatabase } from "@/lib/db/connection";
import { EventModel } from "@/lib/db/models/Event";
import { PartnerModel } from "@/lib/db/models/Partner";
import { createEventSchema } from "@/lib/db/schemas";
import { errorResponse, successResponse } from "@/lib/utils";
export async function POST(request: NextRequest) {
try {
const session = await auth();
const role = (session?.user as { role?: string })?.role;
if (!role || !["admin", "super_admin", "organizer"].includes(role)) {
return errorResponse("Forbidden", 403);
}
const body = await request.json();
const parsed = createEventSchema.safeParse(body);
if (!parsed.success) return errorResponse(parsed.error.errors[0].message, 422);
await connectToDatabase();
const userId = (session!.user as { id: string }).id;
const eventData = { ...parsed.data, organizers: [userId] };
const event = await EventModel.create(eventData);
if (parsed.data.partners?.length) {
await PartnerModel.updateMany(
{ _id: { $in: parsed.data.partners } },
{ $addToSet: { "engagement.eventsParticipated": event._id } }
);
}
return successResponse(event, 201);
} catch (error) {
console.error("POST /api/admin/events error:", error);
return errorResponse("Internal server error", 500);
}
}
Pattern 5: Registration Flow
const registrationSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
password: z.string().min(8).optional(),
skills: z.array(z.string()).min(1).max(10),
experienceLevel: z.enum(["beginner", "intermediate", "advanced"]).optional(),
github: z.string().optional(),
bio: z.string().max(1000).optional(),
customAnswers: z.record(z.string(), z.unknown()).optional(),
});
Pattern 6: Judging Rubric and Score Submission
judgingRubric: [{
name: "Innovation",
description: "How novel...",
weight: 2,
maxScore: 10,
}]
const submitScoreSchema = z.object({
projectId: z.string(),
rubricId: z.string(),
scores: z.array(z.object({
criteriaId: z.string(),
score: z.number().min(0),
feedback: z.string().max(2000),
})),
overallComments: z.string().max(5000),
});
Pattern 7: Zod Schemas
export const createEventSchema = z.object({
name: z.string().min(3).max(200),
description: z.string().min(10).max(5000),
theme: z.string().min(2).max(200),
startDate: z.string().datetime(),
endDate: z.string().datetime(),
registrationDeadline: z.string().datetime(),
location: z.string().min(2).max(500),
capacity: z.number().int().positive().max(10000),
isVirtual: z.boolean(),
tags: z.array(z.string()).max(20),
rules: z.string().max(10000).optional(),
judging_criteria: z.array(z.string()).max(20).optional(),
});
export const updateEventSchema = createEventSchema.partial();
Related API Routes
src/app/api/admin/events/
├── route.ts # GET (list), POST (create)
└── [eventId]/
├── route.ts # GET, PATCH, DELETE
├── assignments/route.ts # Judge-to-project assignments
├── atlas-provisioning/route.ts # Cluster provisioning config
├── feedback-forms/route.ts # Link feedback forms to event
├── feedback-responses/route.ts # Get feedback submissions
├── results/route.ts # Publish final rankings
├── send-feedback/route.ts # Trigger email feedback requests
├── generate-all-feedback/route.ts # AI feedback synthesis
├── landing-page/route.ts # Landing page CRUD
└── publish-results/route.ts # Finalize results
Dependencies
No additional dependencies beyond the scaffold (mongoose, zod, next-auth).
Common Pitfalls
- Check event status before allowing registration. Only
"open" events accept new registrations.
- Use atomic transactions for new user creation during registration to prevent orphaned records.
- Store custom answers keyed by eventId (
customResponses[eventId]) so multiple event registrations don't conflict.
- Use
$addToSet for partner engagement sync — not $push — to prevent duplicate entries.
- Use
sparse: true on landingPage.slug index because not all events have landing pages.
- Validate capacity at registration time, not just on the frontend. Race conditions can cause over-registration.