Flows

Membership Flow

How users become BizTech members, including pricing, payment processing, and what records get created.


What "Membership" Means in the Database

A paid member has records across three tables:

TableKeyWhat's stored
biztechUsersid (email), profileID-index on profileIDStable account identity and canonical profileID.
biztechMembers2027id (email)Yearly membership form data; row existence means current membership for 2027.
biztechProfilescompositeID (PROFILE#<profileID>), typePublic/private networking profile and connection rows.

Important rule for implementation:

  • User owns profile identity.
  • Membership owns yearly membership data.
  • Profile owns networking and public profile data.

Membership Pricing

Defined in services/payments/constants.js:

User typePrice
Non-UBC student$15.00 CAD (MEMBERSHIP_PRICE = 1500 cents)
UBC student$12.00 CAD ($3 discount)

The discount is applied in services/payments/handler.js -> payment() when the user's metadata indicates they are a UBC student.


New Paid Membership Flow

Brand-New User Buying Membership

For a brand-new user buying membership:

  1. Stripe checkout completes.
  2. The payments webhook receives checkout.session.completed.
  3. Create a biztechUsers record with the account fields.
  4. Create a biztechMembers2027 record with profileType: "ATTENDEE".
  5. Call createProfile(email, "ATTENDEE").
  6. createProfile reads membership form data.
  7. Create the biztechProfiles profile row.
  8. Write the generated profileID to biztechUsers.profileID.
Stripe checkout.session.completed
    |
    v
payments webhook
    |
    +-- create biztechUsers { id: email, ... }
    +-- create biztechMembers2027 { id: email, profileType: "ATTENDEE", ... }
    +-- createProfile(email, "ATTENDEE")
          +-- read membership form data
          +-- create biztechProfiles row
          +-- update biztechUsers.profileID

Existing User Buying 2027 Membership

For an existing user buying 2027 membership:

  1. Stripe checkout completes.
  2. The payments webhook receives checkout.session.completed.
  3. Update the existing biztechUsers row with the latest basic fields.
  4. Create a biztechMembers2027 record with profileType: "ATTENDEE".
  5. Read biztechUsers.profileID.
  6. If profileID exists, update the existing biztechProfiles row with pronouns, major, and year.
  7. If profileID does not exist, call createProfile(email, "ATTENDEE").
Stripe checkout.session.completed
    |
    v
payments webhook
    |
    +-- update biztechUsers { latest basic fields, ... }
    +-- create biztechMembers2027 { id: email, profileType: "ATTENDEE", ... }
    +-- read biztechUsers.profileID
          +-- exists: update biztechProfiles pronouns/major/year
          +-- missing: createProfile(email, "ATTENDEE")

Path 3: Admin Grant (No Payment)

Admins can grant membership without payment via POST /members/grant.

Handler: services/members/handler.js -> grantMembership Auth: Cognito (admin-only)

Steps:

  1. Creates or updates biztechUsers with the latest basic fields.
  2. Creates a yearly row in biztechMembers2027 and sets profileType: "ATTENDEE" unless another profile type is intentionally provided.
  3. Reads biztechUsers.profileID.
  4. Updates the existing biztechProfiles row if profileID exists.
  5. Calls createProfile(email, "ATTENDEE") if profileID is missing.

This is the only membership path that does not go through Stripe.


The createProfile() Helper

All membership paths that need a new profile end with createProfile(email, profileType). This function:

  1. Reads the yearly membership form data from biztechMembers2027.
  2. Generates a human-readable profileID using the human-id library (for example, SillyPandasDeny).
  3. Copies firstName, lastName, pronouns, year, major, and profileType into a profile row.
  4. Sets the default viewableMap privacy toggles.
  5. Puts a new profile in biztechProfiles with compositeID: PROFILE#<profileID> and type: PROFILE.
  6. Updates biztechUsers.profileID with the generated profile ID.

Profile Identity Ownership

createProfile() must write the generated profile ID to biztechUsers.profileID. Do not write or read profileID on the yearly membership table as the source of truth.


Table Writes

StepTableWrite
Create or update userbiztechUsersLowercase email in id, latest basic fields, and generated profileID when a new profile is created.
Create yearly membershipbiztechMembers2027Lowercase email in id, membership form answers, and profileType: "ATTENDEE".
Create profilebiztechProfilesProfile row with compositeID: "PROFILE#{profileID}", type: "PROFILE", explicit profileID, and profile fields from the membership form.
Refresh profilebiztechProfilesExisting profile row refreshed with membership fields such as pronouns, major, and year.

Lookup Rules

NeedRead fromNotes
Find user by emailbiztechUsers.idEmail should be lowercase.
Check current membershipbiztechMembers2027.idRow existence means the user is a current member.
Find a user's profile IDbiztechUsers.profileIDThis is the source of truth for profile identity.
Find user/email from profile IDbiztechUsers.profileID-indexUse the profileID-index GSI.
Find yearly membershipbiztechMembers2027.idMembership is keyed by lowercase email.
Find profile rowbiztechProfilesRead compositeID: "PROFILE#{profileID}" and type: "PROFILE".

Members Table Fields

A record in biztechMembers2027:

FieldTypeNotes
idStringLowercase email. Primary key.
profileTypeStringAllowed values are "ATTENDEE", "EXEC", and "PARTNER". Normal paid membership should set "ATTENDEE".
educationStringMembership form value.
firstNameStringMembership form first name.
lastNameStringMembership form last name.
pronounsStringUsed to refresh profile.
studentNumberString/NumberMembership form student number.
facultyStringMembership form faculty.
yearString/NumberUsed to refresh profile.
majorStringUsed to refresh profile.
prevMemberBooleanPrevious member answer.
internationalBooleanInternational student answer.
topicsString[]Interest topics.
heardFromStringReferral source.
heardFromSpecifyStringReferral detail.
dietStringDietary restrictions.
universityStringSchool/university.
highSchoolStringHigh school, if applicable.
adminBooleanExisting admin snapshot.
cardCountNumberNFC/card tracking; defaults to 0.
discordIdStringExisting location for now, though eventually user-level is cleaner.
createdAtNumberEpoch ms.
updatedAtNumberEpoch ms.

Primary key: id

Profile Identity

profileID is not part of the current biztechMembers2027 row shape. Read it from biztechUsers.


Frontend Integration

The membership purchase flow lives in:

  • src/pages/membership.tsx - the membership purchase page
  • src/lib/registrationStrategy/registrationStateOld.ts - handles the Stripe redirect

The page collects user data, determines the correct payment path, POSTs to /payments, and redirects to Stripe. After payment, the webhook is responsible for creating or updating biztechUsers, biztechMembers2027, and biztechProfiles.


Previous
Account Creation