Skip to main content

Meeting request rules

Meetings are short by default, in the style of speed networking. A request is one of two types, its length comes from the type and the boost, and a confirmed meeting blocks the time for both people. Migrations V155 and V156 own the rules; the app mirrors them only to preview what the server will do.

SourceWhat it owns
db/migrations/V155__boosted_meeting_requests.sqlTypes, duration, priority, coffee gate
db/migrations/V156__mutual_meeting_blocking.sqlMutual agenda blocking, interval overlap, slot grid
db/tests/meeting_request_rules.sqlRollback-only SQL test of both migrations
apps/mobile-app/lib/meeting-rules.tsTypeScript mirror used for UI previews

Meeting types​

Type (meeting_type)LengthWho can book it
speed — Quick stand-up (default)5 minutes, extended by boost up to 30Every pass
coffee — Coffee break15, 30 or 45 minutes, chosen by the requesterbusiness and vip passes only

Older free-text types (networking, business, partnership, ...) are not valid any more. The server normalises anything that is not coffee to speed, and existing rows keep the value they were stored with.

Duration and boost​

public.meeting_request_duration(type, boost_points, requested_minutes) is the single source of truth.

Quick stand-up: 5 + 5 per full 10 boost points, capped at 30.

Boost points0–910–1920–2930–3940–4950
Minutes51015202530

Coffee break: the requested length snaps down to a 15-minute step and is clamped to 15–45. Boost does not change a coffee break's length.

Priority: priority_score = 50 + boost points for both types. The speaker's incoming list is ordered by priority_score (highest first), then newest first. The points are charged to the pass (used_boost_amount) in the same transaction that stores the request, and the request fails if the pass does not have them. The drawer caps one request at 50 points, the amount that already buys the 30-minute maximum. That cap is a UI limit: the server only limits boost by the pass balance, so a modified client can spend more points for a higher priority_score (the length stays capped at 30).

The client may send durationMinutes, but the server derives the length and stores its own value, so a modified client cannot buy a longer meeting than its boost pays for.

Coffee breaks are gated on the server​

The coffee gate does not trust the client. insert_meeting_request reads the requester's active pass for the event from the database, and refuses a coffee request unless it is business or vip. It raises SQLSTATE HP403, before the request is stored and before any quota is consumed.

The API returns 403 with:

{ "error": "Coffee meetings require a business or VIP pass", "code": "coffee_requires_business_pass" }

The app also hides the option for general passes and re-checks the pass tier before sending, but those are conveniences, not the rule.

Confirmed meetings block both people​

When a speaker accepts a request (accept_meeting_request), the whole interval [start, start + duration) is blocked for the speaker and for the requester:

  • each person gets a booked row in meeting_slots and a row in meetings;
  • nobody else can book either person for any overlapping time, whether the other person is the host or the requester of a meeting;
  • every conflict check compares intervals, not exact start times, so a 10-minute stand-up at 10:03 is refused if 10:00–10:05 is taken;
  • cancelling a request, or resolving a conflict by dropping a meeting, frees both sides. The host's slot returns to the open pool and the requester's blocking row is removed.

Outcomes of an accept:

SituationResult
Speaker is free, requester is freeconfirmed, both blocked
Speaker already has an overlapping meetingRefused with speaker_slot_conflict
Speaker has an overlapping agenda session of their own (anything beyond "interested")Refused with agenda_slot_conflict
Requester already has an overlapping meetingAccepted as tentative; the requester is notified and must resolve the conflict (requires_resolution: true)

Blocking uses block_meeting_interval and release_meeting_interval. Both are internal helpers with EXECUTE revoked from PUBLIC, anon and authenticated; only the functions above call them.

Slot grid​

get_speaker_available_slots offers start times on a grid that follows the meeting length, so short meetings are not forced onto quarter-hours:

Meeting lengthGrid
5 minutesevery 5 minutes
10 minutesevery 10 minutes
15 minutes and longerevery 15 minutes

A slot is hidden when the speaker, or the requesting user, is busy at any point in the meeting. GET /meetings/requests/slots defaults durationMinutes to 5.

API contract​

POST /api/events/:eventId/meetings/requests

FieldRule
meetingTypespeed (default) or coffee; anything else is treated as speed
durationMinutesWhole number from 5 to 45. Default 5 (speed) or 15 (coffee). Re-derived by the server
boostAmountWhole, non-negative; limited by the pass's remaining boost points (the drawer caps one request at 50)

The response reports the length the server stored in durationMinutes. Errors: 400 for a duration outside 5–45, 403 for the coffee gate above.

In the app​

The request drawer (MeetingRequestSheet) starts with the meeting type. Coffee shows 15/30/45 length chips and is locked with a hint for general passes. The boost bar shows 5 min → N min for a stand-up, or the chosen length for coffee, together with the queue-priority tier. Strings live in networking.meetingRequestModal (English, Spanish and Korean; other languages fall back to English).

Testing​

pnpm --filter hashpass-mobile-app exec jest tests/lib/meeting-rules.test.ts \
tests/components/MeetingRequestSheet.test.tsx tests/api/meeting-requests.test.ts \
tests/api/meeting-request-slots.test.ts tests/api/admin-matchmaking.test.ts
psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f db/tests/meeting_request_rules.sql

The SQL test creates its own event, users, speakers and passes and rolls everything back. Run it on a database with the production-shaped core schema.

:::caution Shared dev project drift The shared development Supabase project lags the migrations: its meeting_requests table is missing the V038 columns (speaker_name, requester_name, priority_score, meeting_id, ...) and its meeting_request_status enum has no requested value. V155/V156 were verified on a production-shaped copy only, so repair that drift before applying them to the dev project, and expect meeting_request_rules.sql to fail there until then. :::