# Feature Specification: Limit Active Events Per Instance **Feature**: `019-instance-limit` **Created**: 2026-03-06 **Status**: Draft **Source**: Migrated from spec/userstories.md (US-13) ## User Scenarios & Testing ### User Story 1 - Enforce Configured Event Cap on Creation (Priority: P1) As a self-hoster, I want to configure a maximum number of simultaneously active events via a server environment variable, so that I can prevent storage exhaustion and limit potential abuse on my instance without modifying code. **Why this priority**: The event cap is the primary deliverable of this story — without it, there is no feature. All other scenarios are edge cases of this core enforcement behavior. **Independent Test**: Can be fully tested by configuring `MAX_ACTIVE_EVENTS=1`, creating one event, then attempting to create a second — the second creation should be rejected with a clear error. **Acceptance Scenarios**: 1. **Given** the server is configured with `MAX_ACTIVE_EVENTS=3` and 3 non-expired events exist, **When** a user submits the event creation form, **Then** the server rejects the request with a clear error indicating the instance is at capacity, and the frontend surfaces this error on the creation form — not as a silent failure. 2. **Given** the server is configured with `MAX_ACTIVE_EVENTS=3` and 2 non-expired events exist, **When** a user submits the event creation form, **Then** the request succeeds and the new event is created normally. 3. **Given** the server is configured with `MAX_ACTIVE_EVENTS=3` and 3 non-expired events exist, but 1 is past its expiry date (awaiting cleanup), **When** a user submits the event creation form, **Then** the request succeeds — expired events do not count toward the limit. --- ### User Story 2 - No Limit When Variable Is Unset (Priority: P2) As a self-hoster running a personal or trusted-group instance, I want no event limit applied by default, so that I do not need to configure anything to run the app normally. **Why this priority**: The default behavior (unlimited) must be safe and require no configuration. Self-hosters who do not need a cap should not have to think about this setting. **Independent Test**: Can be fully tested by starting the server without `MAX_ACTIVE_EVENTS` set and verifying that multiple events can be created without rejection. **Acceptance Scenarios**: 1. **Given** the server has no `MAX_ACTIVE_EVENTS` environment variable set, **When** any number of events are created, **Then** no capacity error is returned — event creation is unlimited. 2. **Given** the server has `MAX_ACTIVE_EVENTS` set to an empty string, **When** events are created, **Then** no capacity error is returned — an empty value is treated the same as unset. --- ### User Story 3 - Cap Is Enforced Server-Side Only (Priority: P2) As a self-hoster, I want the event cap to be enforced exclusively on the server, so that it cannot be bypassed by a modified or malicious client. **Why this priority**: Client-side enforcement alone would be trivially bypassable. The server is the authoritative enforcement point. **Independent Test**: Can be fully tested by sending a direct HTTP POST to the event creation endpoint (bypassing the frontend entirely) when the cap is reached — the server must reject it. **Acceptance Scenarios**: 1. **Given** the configured cap is reached, **When** a direct HTTP POST is made to the event creation endpoint (bypassing the frontend), **Then** the server returns an error response indicating the instance is at capacity. 2. **Given** the configured cap is reached, **When** no personal data is included in the rejection response or logs, **Then** the server returns only the rejection status — no PII is logged. --- ### Edge Cases - What happens when `MAX_ACTIVE_EVENTS=0`? [NEEDS EXPANSION — treat as "no limit" or "reject all"? Clarify during implementation.] - What happens when `MAX_ACTIVE_EVENTS` is set to a non-integer value? The server should fail fast at startup with a clear configuration error. - Race condition: two concurrent creation requests when the cap is at N-1. The server must handle this atomically — one request succeeds, the other is rejected. - Expired events that have not yet been cleaned up must not count toward the limit. The check must query only non-expired events. ## Requirements ### Functional Requirements - **FR-001**: The server MUST read a `MAX_ACTIVE_EVENTS` environment variable at startup to determine the event creation cap. - **FR-002**: If `MAX_ACTIVE_EVENTS` is set to a positive integer and the number of non-expired events equals or exceeds that value, the server MUST reject new event creation requests with a clear error response. - **FR-003**: The frontend MUST surface the capacity error on the event creation form — not as a silent failure or generic error. - **FR-004**: If `MAX_ACTIVE_EVENTS` is unset or empty, the server MUST apply no limit — event creation is unlimited. - **FR-005**: Only non-expired events MUST count toward the limit; expired events awaiting cleanup are excluded from the count. - **FR-006**: The limit MUST be enforced server-side; client-side state or input cannot bypass it. - **FR-007**: No personal data or PII MUST be logged when a creation request is rejected due to the cap. - **FR-008**: The `MAX_ACTIVE_EVENTS` environment variable MUST be documented in the README's self-hosting section (configuration table). ### Key Entities - **Event (active count)**: The count of events whose `expiry_date` is in the future. This is the value checked against `MAX_ACTIVE_EVENTS` at event creation time. ## Success Criteria ### Measurable Outcomes - **SC-001**: When the cap is reached, a POST to the event creation endpoint returns an appropriate HTTP error status with a machine-readable error body. - **SC-002**: The capacity error is displayed to the user on the creation form with a message that does not expose internal state or configuration values. - **SC-003**: Creating events up to but not exceeding the cap succeeds without any change in behavior compared to uncapped instances. - **SC-004**: The `MAX_ACTIVE_EVENTS` variable appears in the README configuration table with its type, default, and description documented. - **SC-005**: Expired events (past `expiry_date`) are never counted toward the cap, verifiable by inspecting the query or checking behavior after expiry.