docs(question-types): document interaction contract

This commit is contained in:
jjbrunton committed 2026-09-05 19:33:17 +01:00
1 parent 53a48ab6d5
commit 450c527d1e
8 files changed
+135 -8

No files matched your search

+1
View File
@@ -20,6 +20,7 @@ historical notes. Agents should load only the section relevant to their task.
- [Couple pairing](couple-pairing.md)
- [Couple streaks](couple-streaks.md)
- [Question selection](question-selection.md)
- [Question interaction types](question-types.md)
- [Match notifications](match-notifications.md)
- [Subscriptions](subscription-system.md)
- [Redemption codes](product/redemption.md)
+45
View File
@@ -0,0 +1,45 @@
# Question interaction types
This is the maintained contract for the question interactions delivered through
the daily question flow. It is separate from the [couples quiz](quiz.md) and
[dares loop](dares-loop.md), which have their own content and completion
models.
<!-- question-types: swipe,text_answer,audio,photo,who_likely -->
## Supported types
| Type | User interaction | `response_data` and `config` | Match semantics |
| --- | --- | --- | --- |
| `swipe` | Choose yes or no, or skip. | No `response_data`; `config` is unused. | A no creates no match. The API also retains `maybe` matching for stored or replayed answers, producing `yes_maybe` or `maybe_maybe`; the current mobile card does not offer maybe. |
| `text_answer` | Write an answer and send it, decline with no, or skip. | `response_data`: `{ type: 'text_answer', text }`; `config` is unused. | Both partners must answer without a no. The match type is `both_answered`, with both payloads in `response_summary`. |
| `audio` | Record and send an audio answer, or skip. | `response_data`: `{ type: 'audio', media_path, duration_seconds }`; optional `config.max_duration_seconds` limits recording and defaults to 60 seconds in the admin UI. | Both partners must answer without a no. The match type is `both_answered`, with both payloads in `response_summary`. |
| `photo` | Take or select and send a photo answer, or skip. | `response_data`: `{ type: 'photo', media_path }`; `config` is unused. | Both partners must answer without a no. The match type is `both_answered`, with both payloads in `response_summary`. |
| `who_likely` | Choose which partner is more likely. The current mobile flow submits this as yes. | `response_data`: `{ type: 'who_likely', chosen_user_id }`; `config` is unused. | Once both partners respond, the match type is `both_answered`; the stored answer value does not gate that type's match calculation. Both choices are in `response_summary`. |
## Persistence and implementation ownership
`questions.question_type` defaults to `swipe`; the database check constraint and
the canonical shared `QuestionType` union must contain the same values.
`questions.config` is JSON configuration for a type. `responses.response_data`
stores an individual non-swipe payload. `matches.response_summary` is a JSON
object keyed by responder user ID and is populated for `both_answered` matches.
The canonical list is `QuestionType` in
[`packages/shared/src/types/index.ts`](../packages/shared/src/types/index.ts).
The database constraint is in
[`apps/api/drizzle/0003_answers_matches.sql`](../apps/api/drizzle/0003_answers_matches.sql).
Match calculation is in
[`apps/api/src/domains/answers/types.ts`](../apps/api/src/domains/answers/types.ts),
the mobile renderer is
[`apps/mobile/src/features/swipe/components/SwipeQuestionCard.tsx`](../apps/mobile/src/features/swipe/components/SwipeQuestionCard.tsx),
and admin authoring options are in
[`apps/admin/src/pages/content/QuestionsPage.tsx`](../apps/admin/src/pages/content/QuestionsPage.tsx).
## Change rule
When adding, removing, or changing a question type, its payload, configuration,
or matching semantics, update this document in the same change. Update the
shared type, database constraint, API, mobile, and admin consumers as required.
`npm run lint:harness` checks the documented type list against the canonical
shared union so an unreviewed list change fails routine verification.
+20 -5
View File
@@ -47,6 +47,8 @@ erDiagram
uuid pack_id FK
text text
text partner_text "for two-part questions"
text question_type "swipe/text_answer/audio/photo/who_likely"
jsonb config "type-specific configuration"
int intensity "1-5"
text[] allowed_couple_genders "couple composition filter"
text[] target_user_genders "individual user filter"
@@ -70,6 +72,7 @@ erDiagram
uuid question_id FK
uuid couple_id FK "nullable: null is a sealed solo answer, claimed at pairing"
answer_type answer "yes/no/maybe"
jsonb response_data "non-swipe answer payload"
timestamptz created_at
}
@@ -77,7 +80,8 @@ erDiagram
uuid id PK
uuid couple_id FK
uuid question_id FK
match_type match_type "yes_yes/yes_maybe/maybe_maybe"
match_type match_type "yes_yes/yes_maybe/maybe_maybe/both_answered"
jsonb response_summary "both non-swipe payloads by user"
boolean is_new
timestamptz created_at
}
@@ -161,7 +165,8 @@ erDiagram
| Enum | Values |
|------|--------|
| `answer_type` | `yes`, `no`, `maybe` |
| `match_type` | `yes_yes`, `yes_maybe`, `maybe_maybe` |
| `question_type` | `swipe`, `text_answer`, `audio`, `photo`, `who_likely` |
| `match_type` | `yes_yes`, `yes_maybe`, `maybe_maybe`, `both_answered` |
| `feedback_type` | `bug`, `feature_request`, `general` |
| `feedback_status` | `new`, `reviewed`, `in_progress`, `resolved`, `closed` |
| `subscription_status` | `active`, `cancelled`, `expired`, `billing_issue`, `paused` |
@@ -188,11 +193,21 @@ allowed.
## Key Flows
### Question interactions
`questions.question_type` defaults to `swipe`; `questions.config` holds
type-specific settings. A non-swipe response keeps its payload in
`responses.response_data`. When a non-swipe interaction resolves as
`both_answered`, `matches.response_summary` holds both payloads keyed by user
ID. The complete interaction and match contract is maintained in
[Question interaction types](question-types.md).
### Response & Match Flow
1. User submits response via `submit-response` edge function
1. User submits a response to the standalone API at `POST /v1/responses`
2. Response saved to `responses` table (UPSERT on `user_id, question_id`)
3. Function checks for partner's response on same question
4. If both answered positively (not "no"), a `match` is created
3. API checks for the partner's response on the same question
4. API applies the [question interaction contract](question-types.md) to create,
update, or remove a `match`
5. Match triggers push notification to both partners
6. Match unlocks chat thread in `messages` table