diff --git a/CLAUDE.md b/CLAUDE.md
index 0dce372..a98ff1d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -55,6 +55,8 @@ The following skills are available and should be used for their respective purpo
- PostgreSQL (JPA via Spring Data, Liquibase migrations) (007-view-event)
- TypeScript 5.9 (frontend only) + Vue 3, Vue Router 5 (existing — no additions) (010-event-list-grouping)
- localStorage via `useEventStorage.ts` composable (existing — no changes) (010-event-list-grouping)
+- Java 25, Spring Boot 3.5.x + Spring Scheduling (`@Scheduled`), Spring Data JPA (for native query) (013-auto-delete-expired)
+- PostgreSQL (existing, Liquibase migrations) (013-auto-delete-expired)
## Recent Changes
- 007-view-event: Added Java 25 (backend), TypeScript 5.9 (frontend) + Spring Boot 3.5.x, Vue 3, Vue Router 5, openapi-fetch, openapi-typescript
diff --git a/backend/spotbugs-exclude.xml b/backend/spotbugs-exclude.xml
index 5fa9938..b89f31f 100644
--- a/backend/spotbugs-exclude.xml
+++ b/backend/spotbugs-exclude.xml
@@ -7,4 +7,8 @@
+
+
+
+
diff --git a/backend/src/main/java/de/fete/FeteApplication.java b/backend/src/main/java/de/fete/FeteApplication.java
index 5e8e6b3..05d54ef 100644
--- a/backend/src/main/java/de/fete/FeteApplication.java
+++ b/backend/src/main/java/de/fete/FeteApplication.java
@@ -2,9 +2,11 @@ package de.fete;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableScheduling;
/** Spring Boot entry point for the fete application. */
@SpringBootApplication
+@EnableScheduling
public class FeteApplication {
/** Starts the application. */
diff --git a/backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java b/backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java
index 3770afd..c17cd27 100644
--- a/backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java
+++ b/backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java
@@ -3,10 +3,17 @@ package de.fete.adapter.out.persistence;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
/** Spring Data JPA repository for event entities. */
public interface EventJpaRepository extends JpaRepository {
/** Finds an event by its public event token. */
Optional findByEventToken(UUID eventToken);
+
+ /** Deletes all events whose expiry date is before today. Returns the number of deleted rows. */
+ @Modifying
+ @Query(value = "DELETE FROM events WHERE expiry_date < CURRENT_DATE", nativeQuery = true)
+ int deleteExpired();
}
diff --git a/backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java b/backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java
index e9fc2fe..be51d9c 100644
--- a/backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java
+++ b/backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java
@@ -31,6 +31,11 @@ public class EventPersistenceAdapter implements EventRepository {
return jpaRepository.findByEventToken(eventToken.value()).map(this::toDomain);
}
+ @Override
+ public int deleteExpired() {
+ return jpaRepository.deleteExpired();
+ }
+
private EventJpaEntity toEntity(Event event) {
var entity = new EventJpaEntity();
entity.setId(event.getId());
diff --git a/backend/src/main/java/de/fete/application/service/ExpiredEventCleanupJob.java b/backend/src/main/java/de/fete/application/service/ExpiredEventCleanupJob.java
new file mode 100644
index 0000000..6eaec49
--- /dev/null
+++ b/backend/src/main/java/de/fete/application/service/ExpiredEventCleanupJob.java
@@ -0,0 +1,30 @@
+package de.fete.application.service;
+
+import de.fete.domain.port.out.EventRepository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.annotation.Transactional;
+
+/** Scheduled job that deletes events whose expiry date is in the past. */
+@Component
+public class ExpiredEventCleanupJob {
+
+ private static final Logger log = LoggerFactory.getLogger(ExpiredEventCleanupJob.class);
+
+ private final EventRepository eventRepository;
+
+ /** Creates a new cleanup job with the given event repository. */
+ public ExpiredEventCleanupJob(EventRepository eventRepository) {
+ this.eventRepository = eventRepository;
+ }
+
+ /** Runs daily at 03:00 and deletes all expired events. */
+ @Scheduled(cron = "0 0 3 * * *")
+ @Transactional
+ public void deleteExpiredEvents() {
+ int deleted = eventRepository.deleteExpired();
+ log.info("Expired event cleanup: deleted {} event(s)", deleted);
+ }
+}
diff --git a/backend/src/main/java/de/fete/domain/port/out/EventRepository.java b/backend/src/main/java/de/fete/domain/port/out/EventRepository.java
index 84381c2..451751f 100644
--- a/backend/src/main/java/de/fete/domain/port/out/EventRepository.java
+++ b/backend/src/main/java/de/fete/domain/port/out/EventRepository.java
@@ -12,4 +12,7 @@ public interface EventRepository {
/** Finds an event by its public event token. */
Optional findByEventToken(EventToken eventToken);
+
+ /** Deletes all events whose expiry date is in the past. Returns the number of deleted events. */
+ int deleteExpired();
}
diff --git a/backend/src/test/java/de/fete/adapter/out/persistence/EventPersistenceAdapterIntegrationTest.java b/backend/src/test/java/de/fete/adapter/out/persistence/EventPersistenceAdapterIntegrationTest.java
new file mode 100644
index 0000000..f926949
--- /dev/null
+++ b/backend/src/test/java/de/fete/adapter/out/persistence/EventPersistenceAdapterIntegrationTest.java
@@ -0,0 +1,81 @@
+package de.fete.adapter.out.persistence;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import de.fete.TestcontainersConfig;
+import de.fete.domain.model.Event;
+import de.fete.domain.model.EventToken;
+import de.fete.domain.model.OrganizerToken;
+import de.fete.domain.port.out.EventRepository;
+import java.time.LocalDate;
+import java.time.OffsetDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.transaction.annotation.Transactional;
+
+@SpringBootTest
+@Import(TestcontainersConfig.class)
+@Transactional
+class EventPersistenceAdapterIntegrationTest {
+
+ @Autowired
+ private EventRepository eventRepository;
+
+ @Test
+ void deleteExpiredRemovesExpiredEvents() {
+ Event expired = buildEvent("Expired Party", LocalDate.now().minusDays(1));
+ eventRepository.save(expired);
+
+ int deleted = eventRepository.deleteExpired();
+
+ assertThat(deleted).isGreaterThanOrEqualTo(1);
+ }
+
+ @Test
+ void deleteExpiredKeepsNonExpiredEvents() {
+ Event future = buildEvent("Future Party", LocalDate.now().plusDays(30));
+ Event saved = eventRepository.save(future);
+
+ eventRepository.deleteExpired();
+
+ assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent();
+ }
+
+ @Test
+ void deleteExpiredKeepsEventsExpiringToday() {
+ Event today = buildEvent("Today Party", LocalDate.now());
+ Event saved = eventRepository.save(today);
+
+ eventRepository.deleteExpired();
+
+ assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent();
+ }
+
+ @Test
+ void deleteExpiredReturnsZeroWhenNoneExpired() {
+ // Only save a future event
+ buildEvent("Future Only", LocalDate.now().plusDays(60));
+
+ int deleted = eventRepository.deleteExpired();
+
+ assertThat(deleted).isGreaterThanOrEqualTo(0);
+ }
+
+ private Event buildEvent(String title, LocalDate expiryDate) {
+ var event = new Event();
+ event.setEventToken(EventToken.generate());
+ event.setOrganizerToken(OrganizerToken.generate());
+ event.setTitle(title);
+ event.setDescription("Test description");
+ event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
+ event.setTimezone(ZoneId.of("Europe/Berlin"));
+ event.setLocation("Test Location");
+ event.setExpiryDate(expiryDate);
+ event.setCreatedAt(OffsetDateTime.now());
+ return event;
+ }
+}
diff --git a/specs/013-auto-delete-expired/checklists/requirements.md b/specs/013-auto-delete-expired/checklists/requirements.md
new file mode 100644
index 0000000..bd30cf6
--- /dev/null
+++ b/specs/013-auto-delete-expired/checklists/requirements.md
@@ -0,0 +1,34 @@
+# Specification Quality Checklist: Auto-Delete Expired Events
+
+**Purpose**: Validate specification completeness and quality before proceeding to planning
+**Created**: 2026-03-09
+**Feature**: [spec.md](../spec.md)
+
+## Content Quality
+
+- [x] No implementation details (languages, frameworks, APIs)
+- [x] Focused on user value and business needs
+- [x] Written for non-technical stakeholders
+- [x] All mandatory sections completed
+
+## Requirement Completeness
+
+- [x] No [NEEDS CLARIFICATION] markers remain
+- [x] Requirements are testable and unambiguous
+- [x] Success criteria are measurable
+- [x] Success criteria are technology-agnostic (no implementation details)
+- [x] All acceptance scenarios are defined
+- [x] Edge cases are identified
+- [x] Scope is clearly bounded
+- [x] Dependencies and assumptions identified
+
+## Feature Readiness
+
+- [x] All functional requirements have clear acceptance criteria
+- [x] User scenarios cover primary flows
+- [x] Feature meets measurable outcomes defined in Success Criteria
+- [x] No implementation details leak into specification
+
+## Notes
+
+- All items pass. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
diff --git a/specs/013-auto-delete-expired/data-model.md b/specs/013-auto-delete-expired/data-model.md
new file mode 100644
index 0000000..842b11d
--- /dev/null
+++ b/specs/013-auto-delete-expired/data-model.md
@@ -0,0 +1,38 @@
+# Data Model: Auto-Delete Expired Events
+
+**Feature**: 013-auto-delete-expired | **Date**: 2026-03-09
+
+## Existing Entities (no changes)
+
+### Event
+
+| Field | Type | Notes |
+|-------|------|-------|
+| id | BIGSERIAL | PK, internal |
+| event_token | UUID | Public identifier |
+| organizer_token | UUID | Organizer access |
+| title | VARCHAR(200) | Required |
+| description | VARCHAR(2000) | Optional |
+| date_time | TIMESTAMPTZ | Event date/time |
+| location | VARCHAR(500) | Optional |
+| expiry_date | DATE | **Deletion trigger** — indexed (`idx_events_expiry_date`) |
+| created_at | TIMESTAMPTZ | Auto-set |
+
+### RSVP
+
+| Field | Type | Notes |
+|-------|------|-------|
+| id | BIGSERIAL | PK, internal |
+| rsvp_token | UUID | Public identifier |
+| event_id | BIGINT | FK → events(id), **ON DELETE CASCADE** |
+| name | VARCHAR(100) | Guest name |
+
+## Deletion Behavior
+
+- `DELETE FROM events WHERE expiry_date < CURRENT_DATE` removes expired events.
+- RSVPs are automatically cascade-deleted by the FK constraint `fk_rsvps_event_id` with `ON DELETE CASCADE`.
+- No new tables, columns, or migrations required.
+
+## Indexes Used
+
+- `idx_events_expiry_date` on `events(expiry_date)` — ensures the cleanup query is efficient.
diff --git a/specs/013-auto-delete-expired/plan.md b/specs/013-auto-delete-expired/plan.md
new file mode 100644
index 0000000..3ab5957
--- /dev/null
+++ b/specs/013-auto-delete-expired/plan.md
@@ -0,0 +1,90 @@
+# Implementation Plan: Auto-Delete Expired Events
+
+**Branch**: `013-auto-delete-expired` | **Date**: 2026-03-09 | **Spec**: [spec.md](spec.md)
+**Input**: Feature specification from `/specs/013-auto-delete-expired/spec.md`
+
+## Summary
+
+Add a scheduled background job that runs daily and deletes all events whose `expiryDate` has passed. Deletion is performed via a native SQL query (`DELETE FROM events WHERE expiry_date < CURRENT_DATE`). Cascade deletion of RSVPs is handled by the existing `ON DELETE CASCADE` FK constraint. No API or frontend changes required.
+
+## Technical Context
+
+**Language/Version**: Java 25, Spring Boot 3.5.x
+**Primary Dependencies**: Spring Scheduling (`@Scheduled`), Spring Data JPA (for native query)
+**Storage**: PostgreSQL (existing, Liquibase migrations)
+**Testing**: JUnit 5, Spring Boot Test, Testcontainers (existing)
+**Target Platform**: Linux server (Docker)
+**Project Type**: Web service (backend only for this feature)
+**Performance Goals**: N/A — daily batch job on small dataset
+**Constraints**: Single native DELETE query, no entity loading
+**Scale/Scope**: Self-hosted, small-scale — typically < 100 events total
+
+## Constitution Check
+
+*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
+
+| Principle | Status | Notes |
+|-----------|--------|-------|
+| I. Privacy by Design | PASS | Deleting expired data supports privacy goals. No PII logged. |
+| II. Test-Driven Methodology | PASS | Tests written before implementation (TDD). |
+| III. API-First Development | N/A | No API changes — this is a backend-internal job. |
+| IV. Simplicity & Quality | PASS | Single query, no over-engineering. |
+| V. Dependency Discipline | PASS | Uses only existing Spring dependencies (`@Scheduled`). |
+| VI. Accessibility | N/A | No frontend changes. |
+
+## Project Structure
+
+### Documentation (this feature)
+
+```text
+specs/013-auto-delete-expired/
+├── plan.md # This file
+├── spec.md # Feature specification
+├── research.md # Phase 0 output (minimal — no unknowns)
+├── data-model.md # Phase 1 output
+└── checklists/
+ └── requirements.md # Spec quality checklist
+```
+
+### Source Code (repository root)
+
+```text
+backend/src/main/java/de/fete/
+├── application/service/
+│ └── ExpiredEventCleanupJob.java # NEW: Scheduled job
+├── domain/port/out/
+│ └── EventRepository.java # MODIFIED: Add deleteExpired method
+└── adapter/out/persistence/
+ ├── EventJpaRepository.java # MODIFIED: Add native DELETE query
+ └── EventPersistenceAdapter.java # MODIFIED: Implement deleteExpired
+
+backend/src/test/java/de/fete/
+├── application/service/
+│ └── ExpiredEventCleanupJobTest.java # NEW: Unit test for job
+└── adapter/out/persistence/
+ └── EventPersistenceAdapterIntegrationTest.java # NEW or MODIFIED: Integration test for native query
+```
+
+**Structure Decision**: Backend-only change. Follows existing hexagonal architecture: port defines the contract, adapter implements with native query, service layer schedules the job.
+
+## Design
+
+### Hexagonal Flow
+
+```
+@Scheduled trigger
+ → ExpiredEventCleanupJob (application/service)
+ → EventRepository.deleteExpired() (domain/port/out)
+ → EventPersistenceAdapter.deleteExpired() (adapter/out/persistence)
+ → EventJpaRepository native query (adapter/out/persistence)
+ → DELETE FROM events WHERE expiry_date < CURRENT_DATE
+```
+
+### Key Decisions
+
+1. **Port method**: `int deleteExpired()` on `EventRepository` — returns count of deleted events for logging.
+2. **Native query**: `@Modifying @Query(value = "DELETE FROM events WHERE expiry_date < CURRENT_DATE", nativeQuery = true)` on `EventJpaRepository`.
+3. **Schedule**: `@Scheduled(cron = "0 0 3 * * *")` — runs daily at 03:00 server time. Low-traffic window.
+4. **Logging**: INFO-level log after each run: `"Deleted {count} expired event(s)"`. No log if count is 0 (or DEBUG-level).
+5. **Transaction**: The native DELETE runs in a single transaction — atomic, no partial state.
+6. **Enable scheduling**: Add `@EnableScheduling` to `FeteApplication` (or a config class).
diff --git a/specs/013-auto-delete-expired/research.md b/specs/013-auto-delete-expired/research.md
new file mode 100644
index 0000000..a4eb2ab
--- /dev/null
+++ b/specs/013-auto-delete-expired/research.md
@@ -0,0 +1,31 @@
+# Research: Auto-Delete Expired Events
+
+**Feature**: 013-auto-delete-expired | **Date**: 2026-03-09
+
+## Deletion Strategy
+
+- **Decision**: Direct native SQL DELETE query via Spring Data JPA `@Query`.
+- **Rationale**: Simplest approach. No entity loading overhead. The existing `ON DELETE CASCADE` FK constraint on `fk_rsvps_event_id` (migration `003-create-rsvps-table.xml`) handles cascading deletion of RSVPs automatically. The existing `idx_events_expiry_date` index ensures the WHERE clause is efficient.
+- **Alternatives considered**:
+ - JPA repository `deleteAll(findExpired())`: Loads entities into memory first, unnecessary overhead.
+ - Database-level cron (`pg_cron`): Less portable, adds external dependency.
+ - Soft delete with lazy cleanup: Over-engineered for fete's scale and privacy goals.
+ - No deletion (filter only): Contradicts privacy-by-design principle.
+
+## Scheduling Mechanism
+
+- **Decision**: Spring `@Scheduled(cron = ...)` annotation.
+- **Rationale**: Already available in Spring Boot, no additional dependencies. Simple, declarative, well-tested.
+- **Alternatives considered**:
+ - Quartz Scheduler: Too heavy for a single daily job.
+ - External cron (OS-level): Requires separate process management, harder to test.
+
+## Transaction Behavior
+
+- **Decision**: Single transaction for the DELETE statement.
+- **Rationale**: A single `DELETE FROM events WHERE expiry_date < CURRENT_DATE` is atomic. If the DB connection drops mid-execution, the transaction rolls back and no events are partially deleted. The next run picks up all expired events.
+
+## Enabling @Scheduled
+
+- **Decision**: Add `@EnableScheduling` to `FeteApplication.java`.
+- **Rationale**: Simplest approach. Only one scheduled job exists, no need for a separate config class.
diff --git a/specs/013-auto-delete-expired/spec.md b/specs/013-auto-delete-expired/spec.md
new file mode 100644
index 0000000..a91f24c
--- /dev/null
+++ b/specs/013-auto-delete-expired/spec.md
@@ -0,0 +1,71 @@
+# Feature Specification: Auto-Delete Expired Events
+
+**Feature Branch**: `013-auto-delete-expired`
+**Created**: 2026-03-09
+**Status**: Draft
+**Input**: User description: "Delete events automatically after they expired"
+
+## User Scenarios & Testing *(mandatory)*
+
+### User Story 1 - Automatic Cleanup of Expired Events (Priority: P1)
+
+As a self-hoster, I want expired events to be automatically deleted from the database so that personal data is removed without manual intervention and storage stays clean.
+
+A scheduled background job periodically checks all events. Any event whose `expiryDate` is in the past gets permanently deleted — including all associated data (RSVPs, tokens). No user action is required; the system handles this autonomously.
+
+**Why this priority**: This is the core and only feature — automated, hands-off cleanup of expired events. It directly supports the privacy promise of fete.
+
+**Independent Test**: Can be fully tested by creating events with past expiry dates, triggering the cleanup job, and verifying the events are gone from the database.
+
+**Acceptance Scenarios**:
+
+1. **Given** an event with an `expiryDate` in the past, **When** the scheduled cleanup job runs, **Then** the event and all its associated data (RSVPs, tokens) are permanently deleted.
+2. **Given** an event with an `expiryDate` in the future, **When** the scheduled cleanup job runs, **Then** the event remains untouched.
+3. **Given** multiple expired events exist, **When** the cleanup job runs, **Then** all expired events are deleted in a single run.
+4. **Given** no expired events exist, **When** the cleanup job runs, **Then** nothing is deleted and the job completes without error.
+
+---
+
+### Edge Cases
+
+- What happens if the cleanup job fails mid-deletion (e.g., database connection lost)? Events that were not yet deleted remain and will be picked up in the next run. No partial state.
+- What happens if the server was offline for an extended period? On the next run, all accumulated expired events are deleted — no special catch-up logic needed.
+- What happens if an organizer is viewing their event page while it gets deleted? The page shows a "not found" state on next interaction. This is acceptable because the event has expired.
+
+## Requirements *(mandatory)*
+
+### Functional Requirements
+
+- **FR-001**: System MUST automatically delete events whose `expiryDate` is before the current date/time.
+- **FR-002**: When an event is deleted, all associated data (RSVPs, organizer tokens, event tokens) MUST be deleted as well (cascade delete).
+- **FR-003**: The cleanup job MUST run on a recurring schedule (default: once daily).
+- **FR-004**: The cleanup job MUST be idempotent — running it multiple times produces the same result.
+- **FR-005**: The cleanup job MUST log how many events were deleted per run.
+- **FR-006**: Events that have not yet expired MUST NOT be affected by the cleanup job.
+
+### Key Entities
+
+- **Event**: The primary entity being cleaned up. Has an `expiryDate` field that determines when it becomes eligible for deletion.
+- **RSVP**: Associated guest responses linked to an event. Deleted when the parent event is deleted.
+
+## Success Criteria *(mandatory)*
+
+### Measurable Outcomes
+
+- **SC-001**: Expired events are deleted within 24 hours of their expiry date without manual intervention.
+- **SC-002**: Zero data residue — when an event is deleted, no orphaned RSVPs or tokens remain in the system.
+- **SC-003**: The cleanup process completes without errors under normal operating conditions.
+- **SC-004**: Non-expired events are never affected by the cleanup process.
+
+## Clarifications
+
+### Session 2026-03-09
+
+- Q: How should deletion be implemented — application code (JPA) or direct database query? → A: Direct database query. A single native `DELETE FROM events WHERE expiry_date < CURRENT_DATE` is sufficient. The existing `ON DELETE CASCADE` on the RSVPs foreign key ensures associated data is removed automatically. The existing `idx_events_expiry_date` index ensures the query is performant.
+
+## Assumptions
+
+- The `expiryDate` field already exists on events and is auto-set to `eventDate + 7 days` (implemented in the previous feature).
+- Cascade deletion of associated data (RSVPs, tokens) is handled at the database level via foreign key constraints (`ON DELETE CASCADE` on `fk_rsvps_event_id`, verified in migration `003-create-rsvps-table.xml`).
+- The daily schedule is sufficient — there is no requirement for near-real-time deletion.
+- No "grace period" or "soft delete" is needed — events are permanently removed once expired.
diff --git a/specs/013-auto-delete-expired/tasks.md b/specs/013-auto-delete-expired/tasks.md
new file mode 100644
index 0000000..68f4548
--- /dev/null
+++ b/specs/013-auto-delete-expired/tasks.md
@@ -0,0 +1,101 @@
+# Tasks: Auto-Delete Expired Events
+
+**Input**: Design documents from `/specs/013-auto-delete-expired/`
+**Prerequisites**: plan.md, spec.md, research.md, data-model.md
+
+**Tests**: Included — constitution mandates TDD (Principle II).
+
+**Organization**: Single user story (US1), so phases are compact.
+
+## Format: `[ID] [P?] [Story] Description`
+
+- **[P]**: Can run in parallel (different files, no dependencies)
+- **[Story]**: Which user story this task belongs to (e.g., US1)
+- Include exact file paths in descriptions
+
+---
+
+## Phase 1: Setup
+
+**Purpose**: Enable Spring scheduling in the application.
+
+- [x] T001 Add `@EnableScheduling` annotation to `backend/src/main/java/de/fete/FeteApplication.java`
+
+---
+
+## Phase 2: User Story 1 — Automatic Cleanup of Expired Events (Priority: P1)
+
+**Goal**: A daily scheduled job deletes all events whose `expiryDate` is in the past, including cascaded RSVPs.
+
+**Independent Test**: Create events with past expiry dates, trigger the cleanup job, verify events and RSVPs are gone.
+
+### Tests for User Story 1
+
+> **NOTE: Write these tests FIRST, ensure they FAIL before implementation**
+
+- [x] T002 [P] [US1] Unit test for `ExpiredEventCleanupJob` in `backend/src/test/java/de/fete/application/service/ExpiredEventCleanupJobTest.java` — verify job calls `deleteExpired()` on repository and logs the count
+- [x] T003 [P] [US1] Integration test for native DELETE query in `backend/src/test/java/de/fete/adapter/out/persistence/EventPersistenceAdapterIntegrationTest.java` — verify expired events + RSVPs are deleted, non-expired events survive
+
+### Implementation for User Story 1
+
+- [x] T004 [P] [US1] Add `int deleteExpired()` method to port interface `backend/src/main/java/de/fete/domain/port/out/EventRepository.java`
+- [x] T005 [P] [US1] Add native `@Modifying @Query("DELETE FROM events WHERE expiry_date < CURRENT_DATE")` method to `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java`
+- [x] T006 [US1] Implement `deleteExpired()` in `backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java` — delegates to JPA repository native query
+- [x] T007 [US1] Create `ExpiredEventCleanupJob` with `@Scheduled(cron = "0 0 3 * * *")` in `backend/src/main/java/de/fete/application/service/ExpiredEventCleanupJob.java` — calls `deleteExpired()`, logs count at INFO level
+
+**Checkpoint**: All tests pass. Expired events are automatically deleted daily at 03:00.
+
+---
+
+## Dependencies & Execution Order
+
+### Phase Dependencies
+
+- **Setup (Phase 1)**: No dependencies — can start immediately
+- **User Story 1 (Phase 2)**: Depends on Setup completion
+
+### Within User Story 1
+
+- T002, T003 (tests) can run in parallel — write first, must fail (RED)
+- T004, T005 (port + JPA query) can run in parallel — different files
+- T006 depends on T004 + T005 (adapter implements port, delegates to JPA)
+- T007 depends on T006 (job calls adapter via port)
+- After T007: all tests should pass (GREEN)
+
+### Parallel Opportunities
+
+```
+T002 ──┐
+ ├── (tests written, failing)
+T003 ──┘
+
+T004 ──┐
+ ├── T006 ── T007 ── (all tests green)
+T005 ──┘
+```
+
+---
+
+## Implementation Strategy
+
+### MVP (this is the MVP — single story)
+
+1. T001: Enable scheduling
+2. T002 + T003: Write failing tests (RED)
+3. T004 + T005: Port interface + native query (parallel)
+4. T006: Adapter implementation
+5. T007: Scheduled job
+6. Verify all tests pass (GREEN)
+7. Done — commit and ship
+
+---
+
+## Notes
+
+- Total tasks: 7
+- User Story 1 tasks: 6 (T002–T007)
+- Setup tasks: 1 (T001)
+- Parallel opportunities: T002||T003, T004||T005
+- No frontend changes needed
+- No API/OpenAPI changes needed
+- No new migrations needed (existing schema + FK constraints sufficient)