Flows
Registration System
The registration system is the most complex flow in the BizTech app. It spans the frontend form, backend validation, capacity management, Stripe payments, email delivery (QR codes + calendar invites), and Slack notifications.
Architecture
Frontend Backend External
βββββββββββββββββ ββββββββββββββββββββββ ββββββββββ
Registration Form ββββ POST /registrations/ ββββ DynamoDB
β basic info β validate event exists SendGrid (QR email)
β custom questions β check existing reg SES (calendar invite)
β payment link? β capacity check SNS β Slack
β write registration Stripe (if paid)
Stripe Checkout ββββ POST /payments/webhook ββββ Update reg status
Components
Frontend
| File | Role |
|---|---|
src/pages/event/[eventId]/[year]/register/index.tsx | Registration page β loads event, checks capacity, renders form |
src/components/Events/AttendeeEventRegistrationForm.tsx | The form component β dynamic questions, Zod validation |
src/lib/registrationStrategy/ | Abstracts free vs paid registration paths |
Backend
| File | Role |
|---|---|
services/registrations/handler.js | All registration handlers: post, put, get, del, delMany, massUpdate, leaderboard |
services/registrations/helpers.js | getEventCounts(), sendDynamicQR(), sendCalendarInvite() |
services/registrations/serverless.yml | Route definitions |
services/payments/handler.js | Stripe checkout session creation and webhook handling |
Database
| Table | Keys | Role |
|---|---|---|
biztechRegistrations | PK: id (email), SK: eventID;year | Stores all registrations |
biztechEvents | PK: id, SK: year | Event config including registrationQuestions and capac |
biztechUsers | PK: id (email) | User profile data (pre-filled in form) |
GSI: event-query on eventID;year for querying all registrations for a single event.
Registration Process
1. Page Load
The registration page fetches:
- Event data from
GET /events/{eventId}/{year}(includesregistrationQuestionsarray) - Current counts to display remaining spots
- User data from
GET /users/{email}to pre-fill basic info - Existing registration from
GET /registrations?email={email}to prevent duplicates
2. Form Rendering
The form has two sections:
Base fields (always present): email, firstName, lastName, yearLevel, faculty, major, pronouns, dietaryRestrictions, howDidYouHear
Dynamic questions (from event.registrationQuestions): Each question has a questionId, type, required flag, and optional choices array. The component generates a Zod schema from these questions at render time.
3. Submission
| Event Type | Path |
|---|---|
Free (pricing === 0) | POST /registrations/ directly β success page |
| Paid | POST /payments β Stripe checkout β webhook confirms β POST /registrations/ |
Payload shape:
{
"email": "student@example.com",
"fname": "Alice",
"eventID": "blueprint",
"year": 2026,
"registrationStatus": "registered",
"basicInformation": { "fname": "Alice", "lname": "Wong", "year": 3, ... },
"dynamicResponses": { "workshop-choice": "Product Management", ... }
}
4. Backend Processing (handler.js β post)
- Validate: Email format, eventID (string), year (number), required fields
- Event check: Query
biztechEventsβ 404 if not found - Duplicate check: Query
biztechRegistrationsfor this email + event- If exists with status
incompleteβ update and return checkout link - If exists with other status β return 400
- If exists with status
- Capacity check:
getEventCounts()counts registrations by status. IfregisteredCount >= event.capacβ change status to"waitlist" - Send emails: QR code email via SendGrid (
sendDynamicQR), calendar invite via SES (sendCalendarInvite). Skipped for:incomplete,rejected,accepted,checkedInstatuses, and for partner registrations - Write to DynamoDB: Create registration in
biztechRegistrations - Slack notification: SNS message with registration details
5. Capacity Management
getEventCounts() in helpers.js returns:
{
registeredCount, checkedInCount, waitlistCount, dynamicCounts
}
dynamicCounts tracks per-question participant caps. For example, if a workshop option has a 30-person limit, the system tracks how many people chose that option.
If the event is full, the handler changes registrationStatus from "registered" to "waitlist" automatically. Admins can manually promote waitlisted users from the event dashboard.
All Registration Endpoints
| Method | Path | Auth | Handler | Description |
|---|---|---|---|---|
| POST | /registrations/ | π | post | Create registration |
| GET | /registrations/ | π | get | Query by email, eventID+year, or all |
| PUT | /registrations/{email}/{fname} | π | put | Update registration (status, points) |
| DELETE | /registrations/{email} | π | del | Delete single registration |
| DELETE | /registrations | π | delMany | Batch delete (admin only) |
| PUT | /registrations/massUpdate | π | massUpdate | Mass update registrations |
| GET | /registrations/leaderboard/ | π | leaderboard | Leaderboard sorted by points |
Registration Status Values
Every registration has a registrationStatus and optionally an applicationStatus:
registrationStatus | Meaning |
|---|---|
"registered" | Confirmed and attending |
"waitlist" | Event was full at registration time |
"incomplete" | Started registration, payment pending (paid events) |
"checkedIn" | Checked in at the event |
"cancelled" | Cancelled by user or admin |
"accepted" | Application accepted, needs payment (paid app events) |
"acceptedPending" | Application accepted, needs confirmation (free app events) |
"acceptedComplete" | Application accepted + confirmed/paid |
applicationStatus | Meaning |
|---|---|
"reviewing" | Application submitted, under review |
"accepted" | Application accepted |
"rejected" | Application rejected |
"waitlist" | Application waitlisted |
End-to-End Trace: Free Event Registration
What happens when a user clicks "Register" on a free event, from button click to database write and email sent:
1. Frontend submit
- User clicks Register β
handleSubmit()inregister/index.tsx(line ~260) - Builds payload with
basicInformation+dynamicResponses - Calls
state.regForFree(payload)onRegistrationStateOld regForFree()setsregistrationStatus: "registered",applicationStatus: ""- POSTs to
/registrationsviafetchBackend
2. Backend handler (post in handler.js)
- Parses body, normalizes email to lowercase
- Validates required fields:
email,eventID,year,registrationStatus - Fetches event from
biztechEventstable β 404 if not found - Checks for existing registration in
biztechRegistrations- If duplicate with
"incomplete"status β returns existingcheckoutLink - If duplicate with other status β returns 400
- If duplicate with
3. Capacity check
- Calls
getEventCounts(eventID, year)inhelpers.js - Queries all registrations for this event via the
event-queryGSI - Counts by status:
registeredCount,checkedInCount,waitlistCount - If
registeredCount >= event.capacβ overrides status to"waitlist"
4. Emails sent
- QR code email via
SESEmailService.sendDynamicQR()β generates a QR image from"email;eventID;year;fname", embeds it inline in an HTML email. Sent via AWS SES (nodemailer), fromdev@ubcbiztech.com - Calendar invite email via
SESEmailService.sendCalendarInvite()β generates an.icsfile with event title, location, start/end time. Attached to email via SES - If waitlisted: QR email still sent (with "waitlist" status text), but no calendar invite
5. Database write
createRegistration()calls DynamoDBUpdateItemonbiztechRegistrations- Key:
{ id: email, "eventID;year": "eventID;year" } - Uses
ConditionExpression: "attribute_not_exists(id)"to prevent overwrites - Sets
createdAttimestamp for new records - Returns 201
6. Slack notification
- Publishes to SNS topic (
process.env.SNS_TOPIC_ARN) - Payload:
{ type: "registration_update", email, eventID, year, registrationStatus, timestamp } - The bots service subscribes to SNS and posts to the Slack channel
- SNS failure is caught and logged β does not fail the registration
7. Frontend redirect
- On success,
router.push(/event/{id}/{year}/register/success)
End-to-End Trace: Paid Event Registration
1. Frontend submit
- Calls
state.regForPaid(payload)β setsregistrationStatus: "incomplete" - POSTs to
/registrationsto create an incomplete record - Then POSTs to
/paymentswithpaymentType: "Event",success_url,email,eventID,year - Backend creates a Stripe Checkout session and returns
session.url - Frontend redirects to Stripe via
window.open(url, "_self")
2. No emails at registration time
sendEmail()skips"incomplete"status entirely β no QR, no calendar invite
3. After Stripe payment
- Stripe sends
checkout.session.completedwebhook toPOST /payments/webhook - Webhook verifies signature, reads
metadata.paymentType === "Event" - Calls
eventRegistration()β queries the existing incomplete registration β updates status to"registered" - The update triggers
sendEmail()again with"registered"status β QR + calendar emails sent
4. Re-registration shortcut
If a user tries to register again while they have an "incomplete" registration, the backend returns the existing checkoutLink (saved Stripe session URL) instead of creating a new one.
Application-Based Events
Application events have an additional review step between registration and confirmation.
Free application event
- Frontend calls
regForFreeApp()β setsregistrationStatus: "registered",applicationStatus: "reviewing" - Backend sends an application confirmation email (not a QR code)
- Admin reviews and accepts β status becomes
"acceptedPending" - User confirms attendance via
confirmAttendance()β status becomes"acceptedComplete"
Paid application event
- Frontend calls
regForPaidApp()β setsregistrationStatus: "incomplete",applicationStatus: "reviewing" - After Stripe payment β
registrationStatusbecomes"registered" - Admin reviews and accepts β
registrationStatusbecomes"accepted" - User pays remaining or confirms β
registrationStatusbecomes"acceptedComplete"
Post-Registration: Check-In
At the event, admins check attendees in via QR scanner or the event dashboard. Check-in calls PUT /registrations/{email}/{fname} with registrationStatus: "checkedIn". After check-in, the NFC popup appears automatically for card writing.
Key Files
| File | What It Does |
|---|---|
bt-web-v2/src/pages/event/[eventId]/[year]/register/index.tsx | Registration page |
bt-web-v2/src/components/Events/AttendeeEventRegistrationForm.tsx | Registration form component β dynamic questions, Zod validation |
bt-web-v2/src/lib/registrationStrategy/ | Free vs paid registration strategy |
serverless-biztechapp-1/services/registrations/handler.js | Backend registration handlers |
serverless-biztechapp-1/services/registrations/helpers.js | getEventCounts(), sendDynamicQR(), sendCalendarInvite() |
serverless-biztechapp-1/services/payments/handler.js | Stripe checkout session creation and webhook handling |
Related Pages
- Event Lifecycle β registration in the context of the full event lifecycle
- Payment Flow β Stripe checkout session creation, webhook handling, and paid registration
- Endpoint Registry β all registration endpoints
- Database Guide β
biztechRegistrationstable details