Flows

Membership Flow

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


What "Membership" Means in the Database

A member has three records across three tables:

TableKeyWhat's stored
biztechUsersid (email)isMember: true
biztechMembers2026id (email)Membership metadata: cardNumber, cardCount, etc.
biztechProfilescompositeID (PROFILE#id), typePublic profile for networking

All three must exist for a user to be fully "a member." The membership flows below always create or update all three.


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.jspayment() when the user's metadata indicates they are a UBC student.


Path 1: New User + Membership Payment (UserMember)

This is the most common path — a brand new user who signs up and pays for membership in one flow.

Frontend (membership page)

    ├─ POST /payments { paymentType: "UserMember", email, fname, ... }


Backend creates Stripe Checkout Session
- line_items: membership price
- metadata: all user fields + paymentType
- success_url, cancel_url


User completes payment on Stripe


Stripe fires checkout.session.completed webhook


webhook()userMemberSignup()

    ├─ 1. Cognito.signUp(email, password)         ← creates auth account
    ├─ 2. OAuthMemberSignup(metadata)
    │      ├─ db.create(USERS_TABLE, { ...userData, isMember: true })
    │      ├─ db.create(MEMBERS2026_TABLE, { id: email, cardCount: 0, ... })
    │      └─ createProfile(email, memberData)TransactWrite to PROFILES + MEMBERS2026
    └─ 3. Return 200

Handler: services/payments/handler.jswebhook()userMemberSignup()

The Cognito password comes from the user — it is passed through Stripe session metadata as data.password. The user provides their password on the membership form before checkout.

Path 2: OAuth User + Membership Payment (OAuthMember)

Same as above but the user already has a Cognito account (via Google/Apple OAuth). Skips the Cognito.signUp() step.

webhook()OAuthMemberSignup(metadata)
    ├─ db.create(USERS_TABLE, { ...userData, isMember: true })
    ├─ db.create(MEMBERS2026_TABLE, { id: email, cardCount: 0, ... })
    └─ createProfile(email, memberData)

Trigger: paymentType: "OAuthMember" in the Stripe session metadata.

Path 3: Existing User + Membership Payment (Member)

The user already has a biztechUsers record (e.g., they registered for a past event) but isn't a member yet.

webhook()memberSignup(metadata)
    ├─ db.updateDB(USERS_TABLE, email, { isMember: true })   ← update, not create
    ├─ db.create(MEMBERS2026_TABLE, { id: email, cardCount: 0, ... })
    └─ createProfile(email, memberData)

Trigger: paymentType: "Member" in the Stripe session metadata.


Path 4: Admin Grant (No Payment)

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

Handler: services/members/handler.jsgrantMembership Auth: Cognito (admin-only)

POST /members/grant
{
  "id": "jane@student.ubc.ca",
  "fname": "Jane",
  "lname": "Doe",
  "year": 3,
  "faculty": "Commerce"
}

Steps:

  1. Upserts biztechUsers — sets isMember: true (creates user if they don't exist)
  2. Upserts biztechMembers2026 — sets cardCount: 0 and all provided metadata
  3. Creates biztechProfiles record if one doesn't already exist (calls createProfile())

This is the only membership path that doesn't go through Stripe.


The createProfile() Helper

All membership paths end with createProfile() (in services/profiles/helpers.js). This function:

  1. Generates a human-readable profileID using the human-id library (e.g., SillyPandasDeny)
  2. Copies firstName, lastName, pronouns, year, major, and profileType from the member record
  3. Sets the default viewableMap (privacy toggles for each field)
  4. Uses a DynamoDB TransactWrite to atomically:
    • Put a new profile in biztechProfiles with compositeID: PROFILE#<profileID> and type: PROFILE
    • Update the member record in biztechMembers2026 to set profileID

The transaction ensures the member always has a valid profileID pointing to an existing profile.


Members Table Fields

A record in biztechMembers2026:

FieldTypeNotes
idStringEmail (primary key)
fname, lnameStringName
cardNumberStringPhysical NFC card number (assigned later)
cardCountNumberStarts at 0, incremented when NFC card is written
profileIDStringHuman-readable ID linking to biztechProfiles
year, facultyMixedAcademic info
createdAtNumberTimestamp

GSI: profile-query — indexes profileID so you can look up a member's email from their profile UUID. Used by GET /members/email/{profileID}.


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 paymentType ("UserMember", "OAuthMember", or "Member" based on whether the user exists and how they authenticated), POSTs to /payments, and redirects to Stripe.


Members Service Endpoints

All endpoints in services/members/handler.js are admin-only (require @ubcbiztech.com email):

MethodPathHandlerDescription
POST/memberscreateCreate member record
GET/members/{id}getGet member by email
GET/members/email/{profileID}getEmailFromProfileReverse lookup: profileID → email
GET/membersgetAllList all members
PATCH/members/{id}updateUpdate member fields
DELETE/members/{id}delDelete member
POST/members/grantgrantMembershipGrant membership (upsert all 3 tables)

Checking Membership Status

The public endpoint GET /users/checkMembership/{email} returns a boolean indicating whether isMember is true on the user's biztechUsers record. The frontend uses this during sign-up to determine whether to show the membership purchase option.


Previous
Account Creation