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:
| Table | Key | What's stored |
|---|---|---|
biztechUsers | id (email), profileID-index on profileID | Stable account identity and canonical profileID. |
biztechMembers2027 | id (email) | Yearly membership form data; row existence means current membership for 2027. |
biztechProfiles | compositeID (PROFILE#<profileID>), type | Public/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 type | Price |
|---|---|
| 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:
- Stripe checkout completes.
- The payments webhook receives
checkout.session.completed. - Create a
biztechUsersrecord with the account fields. - Create a
biztechMembers2027record withprofileType: "ATTENDEE". - Call
createProfile(email, "ATTENDEE"). createProfilereads membership form data.- Create the
biztechProfilesprofile row. - Write the generated
profileIDtobiztechUsers.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:
- Stripe checkout completes.
- The payments webhook receives
checkout.session.completed. - Update the existing
biztechUsersrow with the latest basic fields. - Create a
biztechMembers2027record withprofileType: "ATTENDEE". - Read
biztechUsers.profileID. - If
profileIDexists, update the existingbiztechProfilesrow withpronouns,major, andyear. - If
profileIDdoes not exist, callcreateProfile(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:
- Creates or updates
biztechUserswith the latest basic fields. - Creates a yearly row in
biztechMembers2027and setsprofileType: "ATTENDEE"unless another profile type is intentionally provided. - Reads
biztechUsers.profileID. - Updates the existing
biztechProfilesrow ifprofileIDexists. - Calls
createProfile(email, "ATTENDEE")ifprofileIDis 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:
- Reads the yearly membership form data from
biztechMembers2027. - Generates a human-readable
profileIDusing thehuman-idlibrary (for example,SillyPandasDeny). - Copies
firstName,lastName,pronouns,year,major, andprofileTypeinto a profile row. - Sets the default
viewableMapprivacy toggles. - Puts a new profile in
biztechProfileswithcompositeID: PROFILE#<profileID>andtype: PROFILE. - Updates
biztechUsers.profileIDwith 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
| Step | Table | Write |
|---|---|---|
| Create or update user | biztechUsers | Lowercase email in id, latest basic fields, and generated profileID when a new profile is created. |
| Create yearly membership | biztechMembers2027 | Lowercase email in id, membership form answers, and profileType: "ATTENDEE". |
| Create profile | biztechProfiles | Profile row with compositeID: "PROFILE#{profileID}", type: "PROFILE", explicit profileID, and profile fields from the membership form. |
| Refresh profile | biztechProfiles | Existing profile row refreshed with membership fields such as pronouns, major, and year. |
Lookup Rules
| Need | Read from | Notes |
|---|---|---|
| Find user by email | biztechUsers.id | Email should be lowercase. |
| Check current membership | biztechMembers2027.id | Row existence means the user is a current member. |
| Find a user's profile ID | biztechUsers.profileID | This is the source of truth for profile identity. |
| Find user/email from profile ID | biztechUsers.profileID-index | Use the profileID-index GSI. |
| Find yearly membership | biztechMembers2027.id | Membership is keyed by lowercase email. |
| Find profile row | biztechProfiles | Read compositeID: "PROFILE#{profileID}" and type: "PROFILE". |
Members Table Fields
A record in biztechMembers2027:
| Field | Type | Notes |
|---|---|---|
id | String | Lowercase email. Primary key. |
profileType | String | Allowed values are "ATTENDEE", "EXEC", and "PARTNER". Normal paid membership should set "ATTENDEE". |
education | String | Membership form value. |
firstName | String | Membership form first name. |
lastName | String | Membership form last name. |
pronouns | String | Used to refresh profile. |
studentNumber | String/Number | Membership form student number. |
faculty | String | Membership form faculty. |
year | String/Number | Used to refresh profile. |
major | String | Used to refresh profile. |
prevMember | Boolean | Previous member answer. |
international | Boolean | International student answer. |
topics | String[] | Interest topics. |
heardFrom | String | Referral source. |
heardFromSpecify | String | Referral detail. |
diet | String | Dietary restrictions. |
university | String | School/university. |
highSchool | String | High school, if applicable. |
admin | Boolean | Existing admin snapshot. |
cardCount | Number | NFC/card tracking; defaults to 0. |
discordId | String | Existing location for now, though eventually user-level is cleaner. |
createdAt | Number | Epoch ms. |
updatedAt | Number | Epoch 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 pagesrc/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.
Related Pages
- User, Member & Profile Relationships - how the three records relate
- Account Creation - how user records are created
- Profile Sync - how profiles are created and linked
- Payment Flow - Stripe session creation and webhook handling
- Members Service - full member service endpoint reference
- Admin Detection - admin payment bypass and admin-only endpoints