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

FileRole
src/pages/event/[eventId]/[year]/register/index.tsxRegistration page β€” loads event, checks capacity, renders form
src/components/Events/AttendeeEventRegistrationForm.tsxThe form component β€” dynamic questions, Zod validation
src/lib/registrationStrategy/Abstracts free vs paid registration paths

Backend

FileRole
services/registrations/handler.jsAll registration handlers: post, put, get, del, delMany, massUpdate, leaderboard
services/registrations/helpers.jsgetEventCounts(), sendDynamicQR(), sendCalendarInvite()
services/registrations/serverless.ymlRoute definitions
services/payments/handler.jsStripe checkout session creation and webhook handling

Database

TableKeysRole
biztechRegistrationsPK: id (email), SK: eventID;yearStores all registrations
biztechEventsPK: id, SK: yearEvent config including registrationQuestions and capac
biztechUsersPK: 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} (includes registrationQuestions array)
  • 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 TypePath
Free (pricing === 0)POST /registrations/ directly β†’ success page
PaidPOST /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)

  1. Validate: Email format, eventID (string), year (number), required fields
  2. Event check: Query biztechEvents β€” 404 if not found
  3. Duplicate check: Query biztechRegistrations for this email + event
    • If exists with status incomplete β†’ update and return checkout link
    • If exists with other status β†’ return 400
  4. Capacity check: getEventCounts() counts registrations by status. If registeredCount >= event.capac β†’ change status to "waitlist"
  5. Send emails: QR code email via SendGrid (sendDynamicQR), calendar invite via SES (sendCalendarInvite). Skipped for: incomplete, rejected, accepted, checkedIn statuses, and for partner registrations
  6. Write to DynamoDB: Create registration in biztechRegistrations
  7. 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

MethodPathAuthHandlerDescription
POST/registrations/🌐postCreate registration
GET/registrations/🌐getQuery by email, eventID+year, or all
PUT/registrations/{email}/{fname}🌐putUpdate registration (status, points)
DELETE/registrations/{email}πŸ”“delDelete single registration
DELETE/registrationsπŸ”‘delManyBatch delete (admin only)
PUT/registrations/massUpdate🌐massUpdateMass update registrations
GET/registrations/leaderboard/🌐leaderboardLeaderboard sorted by points

Registration Status Values

Every registration has a registrationStatus and optionally an applicationStatus:

registrationStatusMeaning
"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
applicationStatusMeaning
"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() in register/index.tsx (line ~260)
  • Builds payload with basicInformation + dynamicResponses
  • Calls state.regForFree(payload) on RegistrationStateOld
  • regForFree() sets registrationStatus: "registered", applicationStatus: ""
  • POSTs to /registrations via fetchBackend

2. Backend handler (post in handler.js)

  • Parses body, normalizes email to lowercase
  • Validates required fields: email, eventID, year, registrationStatus
  • Fetches event from biztechEvents table β€” 404 if not found
  • Checks for existing registration in biztechRegistrations
    • If duplicate with "incomplete" status β†’ returns existing checkoutLink
    • If duplicate with other status β†’ returns 400

3. Capacity check

  • Calls getEventCounts(eventID, year) in helpers.js
  • Queries all registrations for this event via the event-query GSI
  • 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), from dev@ubcbiztech.com
  • Calendar invite email via SESEmailService.sendCalendarInvite() β€” generates an .ics file 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 DynamoDB UpdateItem on biztechRegistrations
  • Key: { id: email, "eventID;year": "eventID;year" }
  • Uses ConditionExpression: "attribute_not_exists(id)" to prevent overwrites
  • Sets createdAt timestamp 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) β†’ sets registrationStatus: "incomplete"
  • POSTs to /registrations to create an incomplete record
  • Then POSTs to /payments with paymentType: "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.completed webhook to POST /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

  1. Frontend calls regForFreeApp() β†’ sets registrationStatus: "registered", applicationStatus: "reviewing"
  2. Backend sends an application confirmation email (not a QR code)
  3. Admin reviews and accepts β†’ status becomes "acceptedPending"
  4. User confirms attendance via confirmAttendance() β†’ status becomes "acceptedComplete"
  1. Frontend calls regForPaidApp() β†’ sets registrationStatus: "incomplete", applicationStatus: "reviewing"
  2. After Stripe payment β†’ registrationStatus becomes "registered"
  3. Admin reviews and accepts β†’ registrationStatus becomes "accepted"
  4. User pays remaining or confirms β†’ registrationStatus becomes "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

FileWhat It Does
bt-web-v2/src/pages/event/[eventId]/[year]/register/index.tsxRegistration page
bt-web-v2/src/components/Events/AttendeeEventRegistrationForm.tsxRegistration form component β€” dynamic questions, Zod validation
bt-web-v2/src/lib/registrationStrategy/Free vs paid registration strategy
serverless-biztechapp-1/services/registrations/handler.jsBackend registration handlers
serverless-biztechapp-1/services/registrations/helpers.jsgetEventCounts(), sendDynamicQR(), sendCalendarInvite()
serverless-biztechapp-1/services/payments/handler.jsStripe checkout session creation and webhook handling

Previous
Event Lifecycle