# Feature Specification: RSVP to an Event

**Feature**: `008-rsvp`
**Created**: 2026-03-06
**Status**: Draft
**Source**: Migrated from spec/userstories.md

## Clarifications

### Session 2026-03-06

- Q: How should the server deduplicate RSVPs from the same device without accounts? → A: Server generates an `rsvpToken` (UUID) on first RSVP, returns it to the client. Client stores it in localStorage per event. On re-RSVP, client sends the `rsvpToken` to update instead of create. No global device identifier — the token is event-scoped. If localStorage is lost (cleared or different device), a duplicate entry is accepted as a privacy trade-off.
- Q: Should typed token value objects be used in the backend? → A: Yes. Backend: The three token types (EventToken, OrganizerToken, RsvpToken) MUST be modeled as distinct Java record types wrapping UUID, not passed as raw UUID values. Frontend: No branded types — plain string variables with clear naming (eventToken, rsvpToken) are sufficient given TypeScript's structural typing and OpenAPI codegen.
- Q: How should the RSVP interaction be presented on the event page? → A: Fullscreen event presentation (gradient background, later Unsplash). Title prominent at top, key facts below (description, date, attendee count) — spacious layout. Sticky bottom bar with RSVP CTA. Tap opens a bottom sheet with the RSVP form. After RSVP, the bar shows status ("Du kommst!" + edit option) instead of the CTA.
- Q: How does the RSVP form handle declining? → A: There is no explicit "not attending" button. The bottom sheet only offers the attending flow (name + submit). To not attend, the guest simply closes the sheet. Withdrawing an existing RSVP (DELETE with rsvpToken) is out of scope — deferred to a separate edit-RSVP spec.
- Q: Should the attendee name list be publicly visible on the event page? → A: No. Only the attendee count is shown publicly. The full name list is visible only to the organizer (via organizer link). This maximizes guest privacy.
- Q: Should the RSVP endpoint have spam/abuse protection? → A: No. The RSVP endpoint is intentionally unprotected — risk is consciously accepted as a privacy trade-off consistent with the no-account, no-tracking philosophy. Protection measures can be retrofitted in a separate spec if real-world abuse occurs. KISS.
- Q: How is the attendee count delivered and updated? → A: As a new `attendeeCount` field in the existing Event response (no separate endpoint). Loaded once on page load, no polling or WebSocket. After the guest's own RSVP submission, the count is optimistically incremented (+1) client-side. KISS.
- Q: What determines the RSVP cutoff? → A: The event date itself. No separate expiry field. After the event date has passed, RSVPs are blocked (form hidden, server rejects).
- Q: Should the RSVP entity have an `attending` boolean field? → A: No. The server only stores attending RSVPs — existence of an entry implies attendance. No `attending` boolean needed. Deletion of entries (withdrawal) is deferred to the edit-RSVP spec.

## User Scenarios & Testing

### User Story 1 - Submit an RSVP (Priority: P1)

A guest opens an active event page, which presents the event fullscreen (gradient background, title prominent at top, key facts below including attendee count). A sticky bottom bar shows an RSVP call-to-action. Tapping opens a bottom sheet with the RSVP form: name field + submit. The RSVP is sent to the server and persisted. The server returns an rsvpToken which, along with the name, event token, title, and date, is saved in localStorage. After submission, the bottom sheet closes and the sticky bar shows the guest's RSVP status. To not attend, the guest simply closes the sheet — no server request.

**Why this priority**: Core interactive feature of the app. Without it, guests cannot communicate attendance, and the attendee list (US-2) has no data.

**Independent Test**: Can be fully tested by opening an event page and submitting "I'm attending" with a name, then verifying the attendee list updates and localStorage contains the RSVP record.

**Acceptance Scenarios**:

1. **Given** a guest is on an active event page, **When** they tap the RSVP CTA, enter their name, and submit, **Then** the RSVP is submitted to the server, persisted, and the attendee count updates.
2. **Given** a guest has opened the bottom sheet, **When** they leave the name blank and try to submit, **Then** the form is not submitted and a validation message is shown.
3. **Given** a guest has opened the bottom sheet, **When** they close it without submitting, **Then** no server request is made and no state changes.
4. **Given** a guest submits an RSVP, **When** the submission succeeds, **Then** the rsvpToken, name, event token, event title, and event date are stored in localStorage on this device.
5. **Given** a guest submits an RSVP, **When** the submission succeeds, **Then** no account, login, or personal data beyond the entered name is required.

---

### User Story 2 - RSVP Blocked on Expired or Cancelled Events (Priority: P2)

A guest attempts to RSVP to an event that has already expired or has been cancelled. The RSVP form is not shown and the server rejects any submission attempts.

**Why this priority**: Enforces data integrity and respects the event lifecycle. Cancelled event guard deferred until US-18 is implemented.

**Independent Test**: Can be tested by attempting to RSVP to an event whose expiry date has passed and verifying the form is hidden and the server returns a rejection response.

**Acceptance Scenarios**:

1. **Given** an event's expiry date has passed, **When** a guest opens the event page, **Then** the RSVP form is not shown and no RSVP submission is possible.
2. **Given** an event's expiry date has passed, **When** a guest sends a direct RSVP request to the server, **Then** the server rejects the submission with a clear error response.
3. **Given** an event has been cancelled (US-18), **When** a guest opens the event page, **Then** the RSVP form is hidden and no RSVP submission is possible [deferred until US-18 is implemented].

---

### Edge Cases

- What happens when a guest RSVPs on two different devices? Each device stores its own localStorage entry; the server holds both RSVPs as separate entries (no deduplication across devices — accepted privacy trade-off).
- What happens when the server is unreachable during RSVP submission? The submission fails; localStorage is not updated (no optimistic write). The guest sees an error and can retry.
- What happens if localStorage is cleared after RSVPing? The sticky bar shows the CTA again (as if no prior RSVP). A new submission creates a duplicate server-side entry — accepted privacy trade-off.
- What about spam/abuse on the unprotected RSVP endpoint? Risk is consciously accepted (KISS, privacy-first). No rate limiting, no honeypot, no CAPTCHA. Can be retrofitted in a future spec if real-world abuse occurs.

## Requirements

### Functional Requirements

- **FR-001**: The RSVP bottom sheet MUST offer an attending flow only: name field (required, max 100 characters) + submit. There is no explicit "not attending" option — the guest simply closes the sheet.
- **FR-002**: Submission MUST be blocked if the name is blank or exceeds 100 characters.
- **FR-003**: If a prior RSVP for this event exists in localStorage (rsvpToken present), the sticky bottom bar MUST show the guest's RSVP status instead of the initial CTA. Editing the RSVP (name change, withdrawal) is out of scope for this spec.
- **FR-004**: On successful attending RSVP submission, the server MUST persist the RSVP associated with the event and return an rsvpToken.
- **FR-005**: On successful RSVP submission, the client MUST store the guest's name and the server-returned rsvpToken in localStorage, keyed by event token.
- **FR-006**: On successful RSVP submission, the client MUST store the event token, event title, and event date in localStorage (to support the local event overview, US-7).
- **FR-013**: The event page MUST present the event fullscreen with a sticky bottom bar containing the RSVP call-to-action. Tapping the CTA MUST open a bottom sheet with the RSVP form.
- **FR-014**: After successful RSVP submission, the bottom sheet MUST close and the sticky bar MUST transition to showing the RSVP status.
- **FR-009**: The RSVP form MUST NOT be shown and the server MUST reject RSVP submissions after the event's expiry date has passed.
- **FR-010**: The RSVP form MUST NOT be shown and the server MUST reject RSVP submissions if the event has been cancelled [enforcement deferred until US-18 is implemented].
- **FR-011**: RSVP submission MUST NOT require an account, login, or any personal data beyond the entered name.
- **FR-012**: No personal data or IP address MUST be logged on the server when processing an RSVP.
- **FR-015**: The event page MUST show only the attendee count publicly. The full attendee name list is out of scope (see 009-guest-list).

### Key Entities

- **RSVP**: Represents a guest's attendance declaration. Attributes: rsvpToken (server-generated UUID, returned to client), event reference, name (required), creation timestamp. Existence of an entry implies attendance — no `attending` boolean. The rsvpToken is returned to the client for future use (editing/withdrawal in a later spec). Duplicates from lost localStorage or different devices are accepted as a privacy trade-off.
- **RsvpToken**: A server-generated, event-scoped UUID identifying a single RSVP entry. Modeled as a distinct Java record type (alongside EventToken and OrganizerToken). Stored client-side in localStorage per event.

## Success Criteria

### Measurable Outcomes

- **SC-001**: A guest can submit an RSVP (name + submit) from the event page without an account.
- **SC-002**: After submitting, the sticky bar shows the guest's RSVP status (not the CTA).
- **SC-003**: After submitting an RSVP, the local event overview (US-7) can display the event without a server request (event token, title, and date are in localStorage).
- **SC-004**: The RSVP form is not shown on expired events, and direct server submissions for expired events are rejected.
- **SC-005**: No name, IP address, or personal data beyond the submitted name is stored or logged by the server in connection with an RSVP.
