Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
299 changes: 299 additions & 0 deletions README.matchmaking-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
# Matchmaking Client Integration Guide

This document is for frontend/client implementors integrating the matchmaking feature.

## Feature summary

- Participants opt in once.
- Opted-in participants can fetch a random swipe deck.
- A swipe can be `left` or `right`.
- A match is created only when both users swipe `right`.
- Users can list their matches and view details of a specific match.

## Non-negotiable product behavior

- Only confirmed RSVP users can opt in.
- Mentors and admins are excluded from matchmaking.
- Opt-in is irreversible in this version.
- No undo, unmatch, or re-swipe.
- No notification system yet.
- No in-app chat. Contact happens outside the app.

## Auth and request requirements

Matchmaking endpoints rely on the existing session-cookie auth + CSRF middleware.

Client requirements:

1. Send cookies with requests (for `__session`).
2. Send `x-xsrf-token` header for write requests (`POST`) using the token value from the `XSRF-TOKEN` cookie.
3. Use `Content-Type: application/json` for JSON bodies.

If these are missing, the backend may return `401` or `403`.

## Base route

All routes are under:

- `/match`

If your app prefixes API routes (for example `/api`), apply the same prefix as other existing endpoints in your app.

## Endpoint contracts

## 1) Get matchmaking config

- `GET /match/config`

Success:

```json
{
"data": {
"isMatchOpen": true,
"startDate": "...",
"endDate": "..."
}
}
```

Notes:

- If config document does not exist, backend returns:

```json
{
"status": 400,
"error": "Config not found"
}
```

## 2) Get my matchmaking status

- `GET /match/status`

Success:

```json
{
"data": {
"optedIn": true,
"eligible": true
}
}
```

Use this to decide whether to show:

- Ineligible state
- Opt-in CTA
- Swipe experience

## 3) Opt in

- `POST /match/opt-in`
- Body: none

Success:

```json
{
"message": "Opt-in successful"
}
```

Already opted in:

```json
{
"message": "You are already opted in"
}
```

Ineligible:

```json
{
"error": "You are not eligible to opt in"
}
```

Status code: `403`.

## 4) Get swipe deck

- `GET /match/deck?limit=10`
- `limit` is optional. Default = `10`. Backend caps high values.

Success:

```json
{
"data": [
{
"id": "uid_123",
"firstName": "Jane",
"lastName": "Doe",
"school": "Example High School"
}
]
}
```

Important behaviors:

- Returns empty array when exhausted:

```json
{
"data": []
}
```

- Requires opted in (`403` otherwise).
- Requires matchmaking open (`403` otherwise).

## 5) Swipe on a user

- `POST /match/swipe`
- Body:

```json
{
"targetId": "uid_target",
"direction": "left"
}
```

or

```json
{
"targetId": "uid_target",
"direction": "right"
}
```

Success (no match):

```json
{
"matched": false,
"match": null
}
```

Success (new match):

```json
{
"matched": true,
"match": {
"id": "uidA_uidB"
}
}
```

Validation and errors:

- `400` missing fields or invalid direction
- `400` self-swipe not allowed
- `400` already swiped that target
- `400` target unavailable
- `403` not opted in / matchmaking closed
- `429` rate-limited (more than 15 swipes in 60 seconds)

## 6) Get my matches

- `GET /match/matches`

Success:

```json
{
"data": [
{
"id": "uidA_uidB",
"createdAt": 1717000000,
"user": {
"id": "uid_other",
"firstName": "Alex",
"lastName": "Smith",
"school": "Example University"
}
}
]
}
```

Notes:

- This is where the first swiper eventually discovers matches (no push notification yet).

## 7) Get one match detail

- `GET /match/matches/:id`

Success:

```json
{
"data": {
"id": "uidA_uidB",
"createdAt": 1717000000,
"user": {
"id": "uid_other",
"firstName": "Alex",
"lastName": "Smith",
"school": "Example University"
}
}
}
```

Errors:

- `404` if match not found
- `403` if current user is not part of that match

## Recommended client state machine

At page load:

1. Call `GET /match/status`.
2. If `eligible === false`, show ineligible view.
3. If `eligible === true` and `optedIn === false`, show opt-in CTA.
4. After opt-in success, call `GET /match/config` then `GET /match/deck`.
5. If config says closed, show closed state.

During swipe loop:

1. Render current card.
2. On swipe submit `POST /match/swipe`.
3. Optimistically remove card from local deck.
4. If response has `matched: true`, show matched modal/toast.
5. When deck has low remaining cards, prefetch next `GET /match/deck`.
6. If deck becomes empty, show exhausted state.

Matches screen:

1. Call `GET /match/matches`.
2. Render list sorted client-side if desired.
3. Optional detail screen: call `GET /match/matches/:id`.

## UX and error handling guidance

- Handle `429` with a user-friendly cooldown message.
- Handle `403` closed status with a dedicated message, not a generic error.
- Handle `400` already-swiped silently if it occurs during retries.
- Keep copy explicit that opt-in is permanent in this release.

## Deferred items (do not block implementation)

- Discord handle for hackers in match payload
- Incoming likes view
- Notification delivery when matched
- Block/report backend flows
- In-app chat

Build UI with extension points for these additions, but do not wait for them.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ Required environment variables:
- `FIREBASE_CLIENT_EMAIL`
- `NODE_ENV`

## Matchmaking Caveats and Development Notes

1. Match cards are intentionally minimal (`firstName`, `lastName`, `school`) because the current user profile does not include richer dating fields or photo support.
2. Discord handle exposure for matched hackers is deferred; hacker documents do not currently store `discordUsername`.
3. Only the second swiper sees an immediate match response. The first swiper sees new matches on the next `GET /match/matches` fetch because notifications are deferred.
4. Eligibility is snapshotted by opt-in (`matchEnabled`). If a user's RSVP status changes later, they remain in the matchmaking pool by design.
5. Opt-in is irreversible for this MVP. There is no opt-out, undo swipe, or unmatch flow.
6. Deck generation reads all opted-in users plus the caller's prior swipes. This is acceptable for the expected ~500 participant scale.
7. Reporting is frontend-only for now (mailto to organizers). There is no backend reports collection in this phase.
8. Incoming likes are intentionally deferred, but the schema is designed to support it later via `swipes` queries on `targetId` and `direction`.
9. There is no automated test suite in this repository. Validate behavior with Firebase emulators (`cd functions && npm run serve`).
10. `config/matchConfig` is a required runtime document. In local development it can be seeded by `FakeDataPopulator`; production/staging must create and maintain it manually.

## 🤝 Contributing

1. Fork the repository
Expand Down
49 changes: 48 additions & 1 deletion firestore.indexes.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,51 @@
{
"indexes": [],
"indexes": [
{
"collectionGroup": "swipes",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "swiperId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
},
{
"collectionGroup": "notifications",
"queryScope": "COLLECTION",
"fields": [
{
"fieldPath": "userId",
"order": "ASCENDING"
},
{
"fieldPath": "type",
"order": "ASCENDING"
},
{
"fieldPath": "createdAt",
"order": "DESCENDING"
}
]
}
],
"fieldOverrides": []
}
10 changes: 10 additions & 0 deletions firestore.rules
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ rules_version = '2';

service cloud.firestore {
match /databases/{database}/documents {
match /notifications/{notificationId} {
allow read: if request.auth != null &&
request.auth.uid == resource.data.userId;
allow update: if request.auth != null &&
request.auth.uid == resource.data.userId &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["seen"]) &&
request.resource.data.seen is bool;
allow create, delete: if false;
}

match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
Expand Down
3 changes: 2 additions & 1 deletion functions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
"firebase-functions-test": "^3.1.0",
"typescript": "^5.8.2"
},
"private": true
"private": true,
"packageManager": "npm@11.12.1+sha512.cdca14b85d647b3192028d02aadbe82d75f79a446aceea9874be98e6d768f20ebd3555770a48d0e9906106007877bbc690f715e9372f2e2dc644a3c3157fb14c"
}
Loading