Epic 6: Automated Speaker Onboarding - Architecture Plan

Created: 2026-01-22

Last updated: February 24, 2026 Reading time: 28 minutes File size: 57.87 KB

Created: 2026-01-22 Author: Winston (Architect Agent) Status: DRAFT - Pending Review Key Decision: Simplified 6-state workflow model (reduced from 11 states)


Executive Summary

This document outlines the architecture for refactoring speaker onboarding to an automated, self-service workflow. The goal is to reduce organizer workload by ~40% while enabling speakers to independently:

  1. Respond to event invitations (via magic link - no login required)
  2. Provide/update their speaker profile
  3. Submit presentation title and abstract (required, triggers quality review)
  4. Upload presentation files - PPTX, PDF (optional, can be added anytime)

Simplified Workflow Model

6 Core States: IDENTIFIED β†’ INVITED β†’ ACCEPTED β†’ CONFIRMED β†˜ DECLINED β†˜ WITHDREW

Content tracked separately: PENDING β†’ SUBMITTED β†’ APPROVED (with REVISION_NEEDED loop)

This simplified model replaces the previous 11-state machine, providing:

  • Clearer mental model for organizers and speakers
  • Decoupled content workflow allowing multiple revision cycles
  • Easier validation of state transitions

Current State Analysis

What Exists Today

Component Status Location
Speaker Entity βœ… Complete event-management-service/domain/Speaker.java
Speaker CRUD API βœ… Complete GET/POST/PATCH/DELETE /api/v1/speakers/{username}
Speaker Pool (Event-specific) βœ… Complete SpeakerPool.java - brainstorming phase
Speaker Workflow States βœ… Complete SpeakerWorkflowState enum (shared-kernel)
File Upload Infrastructure βœ… Complete ADR-002 presigned URL pattern
Session Materials Upload βœ… Complete Story 5.9 - SessionMaterialsController
Email Service βœ… Complete shared-kernel/EmailService.java (AWS SES)
Domain Events βœ… Complete SpeakerCreatedEvent, SpeakerInvitedEvent

What’s Missing (Gap Analysis)

Component Gap Epic 6 Story
Automated Invitations Manual contact only Story 6.1
Speaker Response Portal No self-service Story 6.2
Material Self-Submission Organizer uploads only Story 6.3
Speaker Dashboard No speaker view Story 6.4
Deadline Reminders Manual follow-up Story 6.5
Magic Link Auth No passwordless auth Story 6.1/6.4

Target Architecture

Simplified Speaker Workflow (6 States)

Design Decision: Use 6 core workflow states for speaker lifecycle, track content progress separately with timestamps/status fields. This simplifies the state machine while maintaining full functionality.

State Machine

        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚  IDENTIFIED  β”‚  Speaker added to pool
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ Send invitation (auto/manual)
               β–Ό
        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
        β”‚   INVITED    β”‚  Awaiting response
        β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚
        β”Œβ”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”
        β–Ό             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   ACCEPTED   β”‚ β”‚   DECLINED   β”‚ (terminal)
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚
       β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β–Ό                 β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  CONFIRMED   β”‚  β”‚   WITHDREW   β”‚ (terminal)
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   (terminal)

State Definitions

# State Description Triggered By Next States
1 IDENTIFIED Speaker added to pool, not yet contacted Organizer adds to brainstorm list β†’ INVITED
2 INVITED Invitation email sent with magic link Automated email or manual outreach β†’ ACCEPTED, DECLINED
3 ACCEPTED Speaker confirmed participation Speaker clicks β€œAccept” in portal β†’ CONFIRMED, WITHDREW
4 DECLINED Speaker rejected invitation (terminal) Speaker clicks β€œDecline” in portal β€”
5 CONFIRMED Speaker in final published agenda (terminal) Event workflow reaches β€œPublished” β€”
6 WITHDREW Speaker backed out after accepting (terminal) Speaker or organizer cancels β€”

Handling TENTATIVE Responses

Design Decision: TENTATIVE is handled as a flag on INVITED state, not a separate state.

When a speaker responds β€œMaybe/Tentative” (backend-only as of 2026-02-11):

  • Workflow state remains INVITED
  • is_tentative = TRUE
  • tentative_reason stores their constraints (e.g., β€œNeed to check travel dates”)
  • Organizer can follow up and speaker can later Accept or Decline
INVITED (is_tentative=false) ──▢ Speaker clicks "Maybe" ──▢ INVITED (is_tentative=true)
                                                                    β”‚
                                                         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                                                         β–Ό                     β–Ό
                                                    ACCEPTED              DECLINED

This keeps the workflow at 6 states while supporting the tentative use case. (Note: UI only shows Accept/Decline as of 2026-02-11)

Session Assignment

Design Decision: Session is assigned at invite time or later by organizer.

Scenario Session Assignment
Invite with specific session session_id set at invite
Invite without session (general pool) session_id NULL, assigned later
Speaker submits content Requires session_id (organizer must assign first)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    SESSION ASSIGNMENT FLOW                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  Option A: Invite with session                                  β”‚
β”‚  ───────────────────────────────                                β”‚
β”‚  Organizer selects session β†’ Invite sent with sessionId         β”‚
β”‚  Speaker accepts β†’ Can immediately submit content               β”‚
β”‚                                                                 β”‚
β”‚  Option B: Invite to general pool                               β”‚
β”‚  ─────────────────────────────────                              β”‚
β”‚  Organizer invites without session β†’ sessionId = NULL           β”‚
β”‚  Speaker accepts β†’ Organizer assigns session later              β”‚
β”‚  Session assigned β†’ Speaker can now submit content              β”‚
β”‚                                                                 β”‚
β”‚  Content submission blocked until session_id is set             β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Content Progress (Tracked Separately)

Content submission is decoupled from workflow state to allow multiple submission/revision cycles:

Field Type Description
content_status ENUM PENDING, SUBMITTED, APPROVED, REVISION_NEEDED
content_submitted_at TIMESTAMP When speaker submitted content
content_approved_at TIMESTAMP When organizer approved content
content_approved_by VARCHAR Username of approving organizer
is_overflow BOOLEAN Speaker accepted but event is full
is_tentative BOOLEAN Speaker responded β€œmaybe” (still in INVITED state)
tentative_reason TEXT Why speaker is tentative (constraints, etc.)
response_deadline TIMESTAMP Deadline for speaker to respond
content_deadline TIMESTAMP Deadline for content submission

High-Level Flow

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                        SPEAKER ONBOARDING WORKFLOW                          β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚  IDENTIFIED  │───▢│   INVITED    │───▢│   ACCEPTED   │───▢│ CONFIRMED β”‚ β”‚
β”‚  β”‚   (Pool)     β”‚    β”‚   (Email)    β”‚    β”‚   (Portal)   β”‚    β”‚  (Final)  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚        β”‚                    β”‚                   β”‚                  β”‚        β”‚
β”‚        β–Ό                    β–Ό                   β–Ό                  β–Ό        β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ SpeakerPool  β”‚    β”‚  AWS SES     β”‚    β”‚  Portal UI   β”‚    β”‚  Profile  β”‚ β”‚
β”‚  β”‚ (Organizer)  β”‚    β”‚  Magic Link  β”‚    β”‚  Response    β”‚    β”‚  Content  β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚  Form        β”‚    β”‚  Upload   β”‚ β”‚
β”‚                                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

                              β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                              β”‚   CONTENT WORKFLOW   β”‚
                              β”‚  (Parallel to State) β”‚
                              β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
                              β”‚                      β”‚
                              β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                              β”‚  β”‚ Profile Update β”‚  β”‚
                              β”‚  β”‚ - Bio          β”‚  β”‚
                              β”‚  β”‚ - Photo        β”‚  β”‚
                              β”‚  β”‚ - Expertise    β”‚  β”‚
                              β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                              β”‚         β”‚           β”‚
                              β”‚         β–Ό           β”‚
                              β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                              β”‚  β”‚ Session Info   β”‚  β”‚
                              β”‚  β”‚ - Title        β”‚  β”‚
                              β”‚  β”‚ - Abstract     β”‚  β”‚
                              β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                              β”‚         β”‚           β”‚
                              β”‚         β–Ό           β”‚
                              β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                              β”‚  β”‚ File Upload    β”‚  β”‚
                              β”‚  β”‚ - PPTX/PDF     β”‚  β”‚
                              β”‚  β”‚ - Handouts     β”‚  β”‚
                              β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                              β”‚         β”‚           β”‚
                              β”‚         β–Ό           β”‚
                              β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
                              β”‚  β”‚ Quality Review β”‚  β”‚
                              β”‚  β”‚ (Organizer)    β”‚  β”‚
                              β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
                              β”‚                      β”‚
                              β”‚  Status: PENDING β†’   β”‚
                              β”‚  SUBMITTED β†’ APPROVEDβ”‚
                              β”‚                      β”‚
                              β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Architecture Components

Purpose: Allow speakers to access the portal without creating an account.

Technical Design:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                   MAGIC LINK AUTHENTICATION                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  Email Link Structure:                                          β”‚
β”‚  https://portal.batbern.ch/speaker/respond/{token}              β”‚
β”‚                                                                 β”‚
β”‚  Token Properties:                                              β”‚
β”‚  - JWT with speaker_invitation claim                            β”‚
β”‚  - Contains: speakerId, eventCode, sessionId, action            β”‚
β”‚  - Expiry: 30 days (configurable)                               β”‚
β”‚  - Single-use for state-changing actions (Accept/Decline)       β”‚
β”‚  - Reusable for read-only actions (View dashboard)              β”‚
β”‚                                                                 β”‚
β”‚  Database Schema:                                               β”‚
β”‚  CREATE TABLE speaker_invitation_tokens (                       β”‚
β”‚      id UUID PRIMARY KEY,                                       β”‚
β”‚      speaker_id UUID NOT NULL,                                  β”‚
β”‚      event_id UUID NOT NULL,                                    β”‚
β”‚      session_id UUID,                                           β”‚
β”‚      token_hash VARCHAR(255) UNIQUE NOT NULL, -- SHA-256        β”‚
β”‚      action VARCHAR(50) NOT NULL, -- RESPOND, SUBMIT, VIEW      β”‚
β”‚      expires_at TIMESTAMP NOT NULL,                             β”‚
β”‚      used_at TIMESTAMP,                                         β”‚
β”‚      created_at TIMESTAMP DEFAULT NOW()                         β”‚
β”‚  );                                                             β”‚
β”‚                                                                 β”‚
β”‚  Security:                                                      β”‚
β”‚  - Token stored as hash (SHA-256)                               β”‚
β”‚  - Rate limiting: 5 requests/minute per token                   β”‚
β”‚  - IP logging for audit trail                                   β”‚
β”‚  - HTTPS only                                                   β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Implementation Location: services/event-management-service/src/main/java/ch/batbern/events/auth/

2. Automated Invitation System

Purpose: Send automated email invitations with unique response links.

Technical Design:

java
// Email Template Structure
public record SpeakerInvitationEmail(
    String speakerName,
    String speakerEmail,
    String eventName,
    String eventDate,
    String sessionTitle,
    String sessionDescription,
    String responseDeadline,
    String acceptLink,      // Magic link for acceptance
    String declineLink,     // Magic link for decline
    String organizerName,
    String organizerEmail
) {}

// Email Service Extension
@Service
public class SpeakerInvitationService {

    @Async
    public void sendInvitation(SpeakerInvitation invitation) {
        // 1. Generate magic links (accept/decline)
        String acceptToken = generateMagicToken(invitation, Action.ACCEPT);
        String declineToken = generateMagicToken(invitation, Action.DECLINE);

        // 2. Build email from template
        String emailBody = templateEngine.process("speaker-invitation", Map.of(
            "acceptLink", buildPortalUrl(acceptToken),
            "declineLink", buildPortalUrl(declineToken),
            // ... other variables
        ));

        // 3. Send via AWS SES
        emailService.sendHtmlEmail(
            invitation.speakerEmail(),
            "BATbern Invitation: " + invitation.eventName(),
            emailBody
        );

        // 4. Emit domain event
        eventPublisher.publish(new SpeakerInvitationSentEvent(...));

        // 5. Update speaker status
        updateSpeakerStatus(invitation.speakerId(), CONTACTED);
    }
}

Email Templates Location: services/event-management-service/src/main/resources/templates/email/

3. Speaker Self-Service Portal

Purpose: Web interface for speakers to respond, update profile, and submit materials.

Frontend Architecture:

web-frontend/src/pages/speaker-portal/
β”œβ”€β”€ SpeakerPortalLayout.tsx       # Layout with minimal nav (no full auth)
β”œβ”€β”€ InvitationResponsePage.tsx    # Accept/Decline (Tentative removed from UI 2026-02-11)
β”œβ”€β”€ ProfileUpdatePage.tsx         # Bio, photo, expertise
β”œβ”€β”€ ContentSubmissionPage.tsx     # Title, abstract, files
β”œβ”€β”€ SpeakerDashboardPage.tsx      # View upcoming/past events
└── components/
    β”œβ”€β”€ ResponseForm.tsx          # Availability, constraints
    β”œβ”€β”€ ProfileForm.tsx           # Speaker profile editor
    β”œβ”€β”€ ContentWizard.tsx         # Multi-step submission
    └── MaterialUpload.tsx        # Reuse FileUpload component

API Endpoints (Speaker-facing):

yaml
# Speaker Portal API (public with magic link auth)

# Respond to invitation
POST /api/v1/speaker-portal/respond
  Body: {
    token: string,
    response: "ACCEPT" | "DECLINE" | "TENTATIVE",
    reason?: string,           # Required if DECLINE or TENTATIVE
    constraints?: string       # Optional: travel, schedule constraints
  }
  Response: 200 OK {
    speakerName: string,
    eventName: string,
    eventDate: string,
    sessionTitle?: string,     # If session assigned
    nextSteps: string[],       # ["Complete your profile", "Submit title and abstract"]
    contentDeadline?: string   # If accepted
  }
  Notes:
    - ACCEPT β†’ workflow_state = ACCEPTED
    - DECLINE β†’ workflow_state = DECLINED
    - TENTATIVE β†’ workflow_state stays INVITED, is_tentative = true

# Get speaker profile (combined User + Speaker data)
GET /api/v1/speaker-portal/profile?token={token}
  Response: 200 OK {
    username: string,
    email: string,
    firstName: string,
    lastName: string,
    bio?: string,
    profilePictureUrl?: string,
    expertiseAreas?: string[],
    speakingTopics?: string[],
    linkedInUrl?: string,
    languages?: string[]
  }

# Update speaker profile (syncs to User entity per ADR-004)
PATCH /api/v1/speaker-portal/profile
  Body: {
    token: string,
    bio?: string,              # Updates User.bio in Company Service
    firstName?: string,        # Updates User.firstName
    lastName?: string,         # Updates User.lastName
    expertiseAreas?: string[], # Updates Speaker.expertiseAreas
    speakingTopics?: string[], # Updates Speaker.speakingTopics
    linkedInUrl?: string,      # Updates Speaker.linkedInUrl
    languages?: string[]       # Updates Speaker.languages
  }
  Response: 200 OK { updated profile }
  Notes:
    - Profile changes sync to User entity (single source of truth)
    - Changes apply to ALL events (not event-specific)

# Submit content (title + abstract)
POST /api/v1/speaker-portal/content
  Body: {
    token: string,
    title: string,             # Max 200 chars
    abstract: string           # Max 1000 chars
  }
  Response: 201 Created {
    submissionId: string,
    version: number,
    status: "SUBMITTED",
    sessionTitle: string
  }
  Error 400: "Session not assigned - contact organizer"
  Notes:
    - Requires session_id on SpeakerPool (organizer must assign first)
    - Sets content_status = SUBMITTED
    - Creates ContentSubmission record with version

# Get presigned URL for file upload
POST /api/v1/speaker-portal/materials/presigned-url
  Body: {
    token: string,
    fileName: string,
    contentType: string        # application/pdf, application/vnd.ms-powerpoint, etc.
  }
  Response: 200 OK {
    uploadUrl: string,         # S3 presigned PUT URL
    uploadId: string,
    expiresIn: number          # seconds until URL expires (900)
  }

# Confirm file upload
POST /api/v1/speaker-portal/materials/confirm
  Body: {
    token: string,
    uploadId: string,
    materialType: "PRESENTATION" | "DOCUMENT" | "VIDEO" | "OTHER"
  }
  Response: 200 OK {
    materialId: string,
    fileName: string,
    cloudfrontUrl: string
  }

4. Content Submission Workflow

Purpose: Collect presentation title, abstract, and files with quality review.

Design Decision: Content progress is tracked separately from workflow state via content_status field. This allows:

  • Multiple submission/revision cycles without changing workflow state
  • Speaker remains in ACCEPTED state while iterating on content
  • Cleaner separation of concerns

Content Status Flow:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              CONTENT STATUS (Independent of Workflow)           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚   Speaker in ACCEPTED state can submit/revise content:          β”‚
β”‚                                                                 β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚   β”‚ PENDING │───▢│ SUBMITTED │───▢│ APPROVED β”‚                 β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚                        β”‚                                        β”‚
β”‚                        β”‚  ⬅─── QUALITY REVIEW HAPPENS HERE      β”‚
β”‚                        β”‚        (Organizer reviews content)     β”‚
β”‚                        β–Ό                                        β”‚
β”‚                 β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                              β”‚
β”‚                 β”‚REVISION_NEEDED │──▢ Speaker fixes ──▢ SUBMITTEDβ”‚
β”‚                 β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                              β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Content Status Definitions:

Status Description Actor Action
PENDING Speaker accepted but hasn’t submitted content yet β€” Speaker submits
SUBMITTED Content awaiting quality review Speaker Organizer reviews
APPROVED Content passed quality review Organizer Done
REVISION_NEEDED Quality review failed, changes requested Organizer Speaker revises β†’ SUBMITTED

Quality Review Process (performed when status = SUBMITTED):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    QUALITY REVIEW CHECKLIST                     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  Organizer reviews content when content_status = SUBMITTED:     β”‚
β”‚                                                                 β”‚
β”‚  REQUIRED:                                                      β”‚
β”‚  β–‘ Title: Clear, descriptive, not promotional                   β”‚
β”‚  β–‘ Abstract: ≀1000 chars, includes lessons learned              β”‚
β”‚  β–‘ Abstract: Not product promotion or sales pitch               β”‚
β”‚                                                                 β”‚
β”‚  OPTIONAL (can be uploaded later):                              β”‚
β”‚  β—‹ Presentation file (PPTX/PDF)                                 β”‚
β”‚  β—‹ Handouts                                                     β”‚
β”‚                                                                 β”‚
β”‚  Actions:                                                       β”‚
β”‚  β”œβ”€ Title + Abstract OK β†’ Set status to APPROVED                β”‚
β”‚  └─ Issues found β†’ Set status to REVISION_NEEDED                β”‚
β”‚                    + Add reviewer_feedback                      β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

File Upload: Presentation files can be uploaded at any time after acceptance, independent of the quality review. Files are tracked via SessionMaterial (Story 5.9) and are not required for content approval.

Database Schema Extension:

Migration Sequence

Migration Purpose
V43 Add speaker content tracking fields to speaker_pool
V44 Create speaker_invitation_tokens table
V45 Migrate existing workflow states (CONTACTED→INVITED, remove obsolete states)
sql
-- ============================================
-- Flyway Migration: V43__Add_speaker_content_tracking.sql
-- ============================================

-- Extend speaker_pool with simplified workflow + content tracking
-- NOTE: workflow_state column may already exist - use IF NOT EXISTS pattern

-- New columns for 6-state workflow
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS content_status VARCHAR(20) DEFAULT 'PENDING';
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS content_submitted_at TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS content_approved_at TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS content_approved_by VARCHAR(255);
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS is_overflow BOOLEAN DEFAULT FALSE;

-- Timestamps for state transitions
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS invited_at TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS accepted_at TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS declined_at TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS withdrew_at TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS confirmed_at TIMESTAMP;

-- Deadline tracking
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS response_deadline TIMESTAMP;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS content_deadline TIMESTAMP;

-- Tentative response tracking (speaker interested but not committed)
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS is_tentative BOOLEAN DEFAULT FALSE;
ALTER TABLE speaker_pool ADD COLUMN IF NOT EXISTS tentative_reason TEXT;

-- Indexes for common queries
CREATE INDEX IF NOT EXISTS idx_speaker_pool_content_status ON speaker_pool(content_status);
CREATE INDEX IF NOT EXISTS idx_speaker_pool_overflow ON speaker_pool(is_overflow) WHERE is_overflow = TRUE;
CREATE INDEX IF NOT EXISTS idx_speaker_pool_response_deadline ON speaker_pool(response_deadline)
    WHERE response_deadline IS NOT NULL;

-- Content submissions (supports versioning for revision cycles)
CREATE TABLE IF NOT EXISTS speaker_content_submissions (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    speaker_pool_id UUID NOT NULL REFERENCES speaker_pool(id) ON DELETE CASCADE,
    session_id UUID REFERENCES sessions(id),
    title VARCHAR(200) NOT NULL,
    abstract TEXT NOT NULL,  -- Max 1000 chars enforced in app
    abstract_char_count INTEGER NOT NULL,
    submission_version INTEGER DEFAULT 1,
    reviewer_feedback TEXT,
    submitted_at TIMESTAMP DEFAULT NOW(),
    reviewed_at TIMESTAMP,
    reviewed_by VARCHAR(255),

    CONSTRAINT chk_abstract_length CHECK (abstract_char_count <= 1000)
);

CREATE INDEX IF NOT EXISTS idx_content_submissions_speaker ON speaker_content_submissions(speaker_pool_id);
CREATE INDEX IF NOT EXISTS idx_content_submissions_session ON speaker_content_submissions(session_id);

-- ============================================
-- Flyway Migration: V44__Create_speaker_invitation_tokens.sql
-- ============================================

CREATE TABLE speaker_invitation_tokens (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    speaker_pool_id UUID NOT NULL REFERENCES speaker_pool(id) ON DELETE CASCADE,
    token_hash VARCHAR(64) NOT NULL UNIQUE,  -- SHA-256 hash
    action VARCHAR(20) NOT NULL,             -- RESPOND, SUBMIT, VIEW
    expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
    used_at TIMESTAMP WITH TIME ZONE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

    CONSTRAINT chk_token_action CHECK (action IN ('RESPOND', 'SUBMIT', 'VIEW'))
);

CREATE INDEX idx_invitation_tokens_hash ON speaker_invitation_tokens(token_hash);
CREATE INDEX idx_invitation_tokens_speaker ON speaker_invitation_tokens(speaker_pool_id);
CREATE INDEX idx_invitation_tokens_expires ON speaker_invitation_tokens(expires_at)
    WHERE used_at IS NULL;

-- ============================================
-- Flyway Migration: V45__Migrate_workflow_states.sql
-- ============================================

-- Migrate existing workflow states to simplified 6-state model
-- Old state β†’ New state mapping:
--   CONTACTED β†’ INVITED
--   READY β†’ INVITED (with is_tentative = true)
--   CONTENT_SUBMITTED β†’ ACCEPTED (content_status = SUBMITTED)
--   QUALITY_REVIEWED β†’ ACCEPTED (content_status = APPROVED)
--   SLOT_ASSIGNED β†’ ACCEPTED (content_status = APPROVED)
--   OVERFLOW β†’ ACCEPTED (is_overflow = true)

UPDATE speaker_pool SET status = 'INVITED' WHERE status = 'CONTACTED';
UPDATE speaker_pool SET status = 'INVITED', is_tentative = TRUE WHERE status = 'READY';
UPDATE speaker_pool SET status = 'ACCEPTED', content_status = 'SUBMITTED' WHERE status = 'CONTENT_SUBMITTED';
UPDATE speaker_pool SET status = 'ACCEPTED', content_status = 'APPROVED' WHERE status = 'QUALITY_REVIEWED';
UPDATE speaker_pool SET status = 'ACCEPTED', content_status = 'APPROVED' WHERE status = 'SLOT_ASSIGNED';
UPDATE speaker_pool SET status = 'ACCEPTED', is_overflow = TRUE WHERE status = 'OVERFLOW';

-- Add constraint to enforce new valid states only
ALTER TABLE speaker_pool ADD CONSTRAINT chk_workflow_state
    CHECK (status IN ('IDENTIFIED', 'INVITED', 'ACCEPTED', 'DECLINED', 'CONFIRMED', 'WITHDREW'));

Workflow + Content Status Combinations:

Workflow State Content Status Meaning Next Action
IDENTIFIED β€” Not yet invited Organizer sends invitation
INVITED β€” Awaiting response Speaker responds
ACCEPTED PENDING Accepted, no content yet Speaker submits content
ACCEPTED SUBMITTED Quality review queue Organizer reviews
ACCEPTED REVISION_NEEDED Changes requested Speaker revises content
ACCEPTED APPROVED Content approved, ready Wait for event finalization
CONFIRMED APPROVED In final agenda Done

Note: Quality review only happens when content_status = SUBMITTED. This is when the organizer:

  1. Reviews title and abstract (required for submission)
  2. Either approves (β†’ APPROVED) or requests revisions (β†’ REVISION_NEEDED with feedback)

File upload is separate: Presentation files (PPTX/PDF) can be uploaded anytime after acceptance and are tracked independently via SessionMaterial. Not required for quality review approval.

5. Automated Deadline Reminders

Purpose: Send configurable reminders before deadlines.

Technical Design:

java
// Reminder Configuration
public record ReminderRule(
    EventType eventType,          // Conference, Workshop, etc.
    ReminderTier tier,            // TIER_1 (friendly), TIER_2 (urgent), TIER_3 (escalate)
    int daysBeforeDeadline,       // e.g., 30, 14, 3
    boolean autoEscalate,         // Escalate to organizer after TIER_3
    String emailTemplate          // Template name
) {}

// Scheduler Job
@Scheduled(cron = "0 0 9 * * *")  // Daily at 9 AM
public void processDeadlineReminders() {
    // 1. Find speakers with upcoming deadlines
    List<SpeakerDeadline> deadlines = findUpcomingDeadlines();

    // 2. Match against reminder rules
    for (SpeakerDeadline deadline : deadlines) {
        ReminderRule rule = findApplicableRule(deadline);
        if (rule != null && !alreadySent(deadline, rule)) {
            sendReminder(deadline, rule);
            logReminder(deadline, rule);
        }
    }
}

Reminder Templates:

  • reminder-tier1.html - Friendly reminder (30 days before)
  • reminder-tier2.html - Urgent reminder (14 days before)
  • reminder-tier3.html - Final reminder with escalation warning (3 days before)

Data Architecture

Entity Relationships (Simplified Model)

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    SPEAKER DOMAIN MODEL (6-State Workflow)                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚    User      │◄───────│     Speaker      β”‚        β”‚   SpeakerPool    β”‚  β”‚
β”‚  β”‚ (Company Svc)β”‚   via  β”‚ (Global Profile) β”‚        β”‚ (Event-specific) β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ usernameβ””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚        β”‚                        β”‚                              β”‚            β”‚
β”‚        β–Ό                        β–Ό                              β”‚            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                  β”‚            β”‚
β”‚  β”‚ Profile Data β”‚        β”‚ Speaker-specific β”‚                  β”‚            β”‚
β”‚  β”‚ - name       β”‚        β”‚ - expertise[]    β”‚                  β”‚            β”‚
β”‚  β”‚ - email      β”‚        β”‚ - topics[]       β”‚                  β”‚            β”‚
β”‚  β”‚ - bio        β”‚        β”‚ - languages[]    β”‚                  β”‚            β”‚
β”‚  β”‚ - photo      β”‚        β”‚ - linkedin       β”‚                  β”‚            β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                  β”‚            β”‚
β”‚                                                                β”‚            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚
β”‚  β”‚                                                                          β”‚
β”‚  β”‚  SpeakerPool (Event-Speaker Association)                                 β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚  β”‚ workflow_state: IDENTIFIED|INVITED|ACCEPTED|DECLINED|CONFIRMED|WITHDREWβ”‚
β”‚  β”‚  β”‚ content_status: PENDING|SUBMITTED|APPROVED|REVISION_NEEDED         β”‚  β”‚
β”‚  β”‚  β”‚ is_overflow: boolean, is_tentative: boolean, tentative_reason: textβ”‚  β”‚
β”‚  β”‚  β”‚ response_deadline, content_deadline                                β”‚  β”‚
β”‚  β”‚  β”‚ invited_at, accepted_at, declined_at, withdrew_at, confirmed_at    β”‚  β”‚
β”‚  β”‚  β”‚ content_submitted_at, content_approved_at, content_approved_by     β”‚  β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”‚           β”‚                              β”‚                               β”‚
β”‚  β”‚           β–Ό                              β–Ό                               β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”‚
β”‚  β”‚  β”‚ ContentSubmissionβ”‚           β”‚  SessionMaterial β”‚                    β”‚
β”‚  β”‚  β”‚ - title          β”‚           β”‚  - file (S3)     β”‚                    β”‚
β”‚  β”‚  β”‚ - abstract       β”‚           β”‚  - type (PPTX,   β”‚                    β”‚
β”‚  β”‚  β”‚ - version        β”‚           β”‚    PDF, VIDEO)   β”‚                    β”‚
β”‚  β”‚  β”‚ - reviewer_      β”‚           β”‚  - cloudfront_urlβ”‚                    β”‚
β”‚  β”‚  β”‚   feedback       β”‚           β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                                   β”‚
β”‚  β”‚                                                                          β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚                                                                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚                    Magic Link Tokens                                 β”‚   β”‚
β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚   β”‚
β”‚  β”‚  β”‚ speaker_invitation_tokens                                      β”‚  β”‚   β”‚
β”‚  β”‚  β”‚ - token_hash (SHA-256)                                         β”‚  β”‚   β”‚
β”‚  β”‚  β”‚ - speaker_pool_id                                              β”‚  β”‚   β”‚
β”‚  β”‚  β”‚ - action: RESPOND|SUBMIT|VIEW                                  β”‚  β”‚   β”‚
β”‚  β”‚  β”‚ - expires_at, used_at                                          β”‚  β”‚   β”‚
β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β”‚                                                                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Design Decisions

  1. 6 Workflow States - Simplified from 11 states for clearer mental model
  2. Content Status Decoupled - Allows multiple revision cycles without state changes
  3. Timestamps per State - Track when each transition occurred
  4. Overflow as Flag - Boolean instead of separate state, applied to ACCEPTED speakers
  5. Versioned Submissions - Support multiple content revisions with reviewer feedback

Speaker-User Assignment

How is a Speaker linked to a User?

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    SPEAKER-USER RELATIONSHIP                    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  Speaker.username ──references──▢ User.username                 β”‚
β”‚  (Event Service)                  (Company Service)             β”‚
β”‚                                                                 β”‚
β”‚  - No database FK (cross-service, ADR-004)                      β”‚
β”‚  - User data fetched via HTTP enrichment                        β”‚
β”‚  - username is the meaningful identifier (ADR-003)              β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Speaker Assignment Flow (for Self-Service Portal):

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              INVITE SPEAKER (Auto-Create User)                  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                 β”‚
β”‚  1. Organizer enters speaker email (+ optional name)            β”‚
β”‚                                                                 β”‚
β”‚  2. System checks: Does User with this email exist?             β”‚
β”‚     β”œβ”€ YES: Link SpeakerPool to existing username               β”‚
β”‚     └─ NO:  Create User with role=SPEAKER                       β”‚
β”‚             └─ Generate username from email (john.doe@x.com     β”‚
β”‚                β†’ john.doe or john.doe.2 if taken)               β”‚
β”‚                                                                 β”‚
β”‚  3. Create SpeakerPool entry:                                   β”‚
β”‚     - event_id: current event                                   β”‚
β”‚     - username: linked user                                     β”‚
β”‚     - workflow_state: IDENTIFIED                                β”‚
β”‚                                                                 β”‚
β”‚  4. Send invitation email with magic link                       β”‚
β”‚     - workflow_state β†’ INVITED                                  β”‚
β”‚                                                                 β”‚
β”‚  5. Speaker clicks link, can:                                   β”‚
β”‚     - Complete profile (updates User entity)                    β”‚
β”‚     - Accept/Decline (updates SpeakerPool)                      β”‚
β”‚     - Submit content (creates ContentSubmission)                β”‚
β”‚                                                                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

User Creation on Invite:

java
// When organizer invites a speaker by email
public SpeakerPool inviteSpeaker(String eventId, String email, String name) {
    // 1. Find or create User
    User user = userService.findByEmail(email)
        .orElseGet(() -> userService.createSpeakerUser(email, name));

    // 2. Create SpeakerPool entry
    SpeakerPool speakerPool = SpeakerPool.builder()
        .eventId(eventId)
        .username(user.getUsername())  // Link via username
        .workflowState(IDENTIFIED)
        .build();

    return speakerPoolRepository.save(speakerPool);
}

// User creation for new speakers
public User createSpeakerUser(String email, String name) {
    String username = generateUsername(email);  // john.doe@x.com β†’ john.doe

    return User.builder()
        .username(username)
        .email(email)
        .firstName(extractFirstName(name))
        .lastName(extractLastName(name))
        .role(Role.SPEAKER)
        .status(UserStatus.PENDING)  // Activated on first portal access
        .build();
}

Benefits of Auto-Create:

  • Organizer just needs email to invite anyone
  • Speaker completes profile themselves via portal
  • No manual user creation step
  • Consistent model (all speakers have User accounts)

API Design (ADR-003 Compliance)

All APIs use meaningful identifiers:

  • Speaker: username (not UUID)
  • Event: eventCode (not UUID)
  • Session: sessionSlug (not UUID)

Security Considerations

Concern Mitigation
Token theft Short expiry (30 days), single-use for state changes
Replay attacks Token invalidated after sensitive actions
Enumeration Rate limiting (5 req/min), no error differentiation
MITM HTTPS-only, HSTS enabled
Phishing Clear sender identity, consistent branding

RBAC for Speaker Portal

java
// Speaker can only access their own data
@PreAuthorize("@speakerPortalSecurity.validateToken(#token)")
public ResponseEntity<?> getProfile(@RequestParam String token) {
    // Token validation extracts speakerId
    // Only returns data for that speaker
}

// Organizer override for assistance
@PreAuthorize("hasRole('ORGANIZER')")
public ResponseEntity<?> getAnyProfile(@PathVariable String username) {
    // Full access for organizers
}

Implementation Phases

Phase 1: Foundation (Week 1-2)

Story 6.1a: Magic Link Infrastructure

  • Create speaker_invitation_tokens
  • Implement MagicLinkService

Story 6.1b: Email Template System

  • Extend EmailService

Phase 2: Response Portal (Week 3-4)

Story 6.2a: Invitation Response

  • InvitationResponsePage.tsx - Accept/Decline UI (Tentative removed from UI 2026-02-11)
  • POST /speaker-portal/respond

Story 6.2b: Profile Update

  • ProfileUpdatePage.tsx

Phase 3: Content Submission (Week 5-6)

Story 6.3a: Title & Abstract

  • ContentSubmissionPage.tsx

Story 6.3b: File Upload

  • Reuse MaterialUpload
  • Speaker-specific S3 path:

Phase 4: Dashboard & Reminders (Week 7-8)

Story 6.4: Speaker Dashboard

  • SpeakerDashboardPage.tsx

Story 6.5: Automated Reminders

  • ReminderSchedulerJob

Integration Points

Existing Services

Service Integration Pattern
Company User Management User data enrichment HTTP client (ADR-004)
Event Management Session/Event data Direct (same service)
AWS SES Email sending Shared-kernel EmailService
AWS S3 File storage Presigned URLs (ADR-002)
CloudFront File delivery CDN distribution

Domain Events

java
// Domain Events for Epic 6 (Simplified 6-State Model)

// Workflow State Changes
public record SpeakerWorkflowStateChangedEvent(
    String speakerPoolId,
    String username,
    String eventCode,
    String sessionId,
    WorkflowState previousState,  // IDENTIFIED, INVITED, ACCEPTED, DECLINED, CONFIRMED, WITHDREW
    WorkflowState newState,
    String changedBy,
    Instant changedAt
) implements DomainEvent {}

// Content Status Changes (separate from workflow)
public record SpeakerContentStatusChangedEvent(
    String speakerPoolId,
    String username,
    String eventCode,
    ContentStatus previousStatus,  // PENDING, SUBMITTED, APPROVED, REVISION_NEEDED
    ContentStatus newStatus,
    String changedBy,
    String feedback,  // Optional: reviewer feedback for REVISION_NEEDED
    Instant changedAt
) implements DomainEvent {}

// Specific Events for Automation
public record SpeakerInvitationSentEvent(
    String speakerPoolId,
    String username,
    String email,
    String eventCode,
    String sessionId,
    String magicLinkToken,
    Instant sentAt,
    Instant expiresAt
) implements DomainEvent {}

public record SpeakerResponseReceivedEvent(
    String speakerPoolId,
    String username,
    String eventCode,
    WorkflowState response,  // ACCEPTED or DECLINED
    String declineReason,    // Optional: if DECLINED
    Instant respondedAt
) implements DomainEvent {}

public record SpeakerContentSubmittedEvent(
    String speakerPoolId,
    String username,
    String eventCode,
    String sessionId,
    String submissionId,
    int submissionVersion,
    Instant submittedAt
) implements DomainEvent {}

public record SpeakerReminderSentEvent(
    String speakerPoolId,
    String username,
    String eventCode,
    ReminderTier tier,  // TIER_1, TIER_2, TIER_3
    ReminderType type,  // RESPONSE_DEADLINE, CONTENT_DEADLINE
    Instant sentAt
) implements DomainEvent {}

public record SpeakerOverflowDetectedEvent(
    String eventCode,
    long acceptedCount,
    int maxSlots,
    List<String> overflowSpeakers,  // usernames marked as overflow
    Instant detectedAt
) implements DomainEvent {}

Workflow State Enum (Simplified)

java
// Update shared-kernel: SpeakerWorkflowState.java
public enum SpeakerWorkflowState {
    /**
     * Speaker added to pool, not yet contacted.
     */
    IDENTIFIED,

    /**
     * Invitation email sent with magic link.
     * Speaker has been contacted and is awaiting response.
     */
    INVITED,

    /**
     * Speaker confirmed participation.
     * Can now submit content (profile, title, abstract, files).
     */
    ACCEPTED,

    /**
     * Speaker rejected invitation.
     * Terminal state - no further transitions.
     */
    DECLINED,

    /**
     * Speaker locked in final published agenda.
     * Terminal state - event is published.
     */
    CONFIRMED,

    /**
     * Speaker backed out after accepting.
     * Terminal state - need replacement speaker.
     */
    WITHDREW
}

// Content status tracked separately
public enum ContentStatus {
    PENDING,          // No content submitted yet
    SUBMITTED,        // Content awaiting review
    APPROVED,         // Content approved by organizer
    REVISION_NEEDED   // Organizer requested changes
}

Testing Strategy

Test Categories

Category Coverage Target Tools
Unit Tests >85% JUnit 5, Mockito
Integration Tests >80% Testcontainers (PostgreSQL)
E2E Tests Critical paths Playwright, Bruno
Security Tests OWASP Top 10 OWASP ZAP, manual review

Key Test Scenarios

  1. Magic Link Flow: Generate β†’ Send β†’ Validate β†’ Expire
  2. Response Flow: Receive email β†’ Click link β†’ Submit response β†’ Notification sent
  3. Content Flow: Login β†’ Edit profile β†’ Submit content β†’ Upload files β†’ Review
  4. Reminder Flow: Configure rules β†’ Scheduler runs β†’ Email sent β†’ Not duplicated

Success Metrics

Metric Target Measurement
Self-service adoption 70%+ Speakers using portal vs manual
Response time <3 days avg Time from invitation to response
Material submission <1 week avg Time from acceptance to content submitted
Organizer workload reduction 40% Hours spent on speaker coordination

Design Decisions (Resolved)

# Question Decision Rationale
1 Profile Sync Sync to User entity Single source of truth (ADR-004). Profile changes apply to all events.
2 TENTATIVE Response Flag on INVITED state Keeps 6 states. is_tentative=true + tentative_reason track β€œmaybe” responses.
3 Session Assignment At invite or later Flexible: invite with session OR assign after acceptance. Content blocked until assigned.
4 Migration Strategy V45 migrates old states Maps 11 states to 6 states with content_status and is_overflow flags.

Open Questions for Discussion

  1. Magic Link Expiry: Should tokens expire after 30 days or 60 days? What’s the typical speaker response window?

    • Current default: 30 days
  2. Quality Review Automation: Should we implement AI-assisted abstract quality checking (length, promotional content detection)?

    • Recommendation: Defer to future enhancement, start with manual review
  3. Hybrid Mode: How should the system behave when some speakers use portal and others prefer email/phone communication?

    • Decision: Organizer can manually transition IDENTIFIED β†’ INVITED β†’ ACCEPTED for speakers who respond offline
    • Manual transitions allowed for organizers via existing status management UI
  4. Undo Declined: If a speaker accidentally declines, can they re-accept?

    • Decision: Organizer can transition DECLINED β†’ INVITED to re-send invitation
    • Speaker receives new magic link email
  5. Overflow Handling: When a speaker is marked as overflow, should they be automatically promoted if another speaker withdraws?

    • Decision: Send notification to organizer, let them decide manually
    • Organizer sets is_overflow=false to promote speaker

Next Steps

  1. Review this plan with product owner and development team
  2. Prioritize stories based on MVP requirements
  3. Create detailed story specs following template in /docs/templates/
  4. Set up feature branch feature/epic-6-speaker-portal
  5. Begin Phase 1 implementation with TDD approach

References

  • Epic 6 PRD: docs/prd/epic-6-speaker-portal-support.md
  • Story 6.0 (Foundation): docs/stories/6.0.speaker-profile-foundation.md
  • Story 5.9 (Materials): docs/stories/5.9-session-materials-upload.md
  • Speaker API Design: docs/architecture/04-api-speaker-coordination.md
  • ADR-002 (File Uploads): docs/architecture/ADR-002-generic-file-upload.md
  • ADR-003 (Meaningful IDs): docs/architecture/ADR-003-meaningful-identifiers-public-apis.md
  • ADR-004 (User Fields): docs/architecture/ADR-004-factor-user-fields-from-domain-entities.md