Story NOTIF-009: Platform kill-switch (pause/resume all dispatch)¶
Module: notifications
Slice: v0.1.0 NOTIF — platform kill-switch (PRD V0 Envelope v0.1.0 "le kill-switch plateforme ... deviennent actifs"; V1 PRD §15.2; NOTIF-US-04 AC-04.5 + NOTIF-US-10)
Side: [BACKEND] (admin endpoint; the admin UI is V1)
Version target: V0
Sub-version: v0.1.0
Priority: 13 (v0.1.0)
Depends on: NOTIF-001a, NOTIF-001b (dispatch + SMS adapter); AUDIT catalog extended with NOTIF_KILL_SWITCH_ENGAGED / NOTIF_KILL_SWITCH_RELEASED
Can develop concurrently with: NOTIF-008, NOTIF-010
Merge order: Any order within v0.1.0
Estimated complexity: M
PRD User Stories: NOTIF-US-10 (AC-10.1, AC-10.2, AC-10.3) + NOTIF-US-04 (AC-04.5)
Wireframe: N/A — backend only (admin UI is V1; operator banner AC-10.4 is OP cross-module, out of scope here)
⚠️ [ARCH-PENDING] — statut PRÉLIMINAIRE¶
Architecture is v0.0.0-only; the kill-switch is "designed-for but inactive" there (architecture §0; the
PENDING_PLATFORM_PAUSEDstatus is reserved in §2.3, and the/admin/kill-switchpath is reserved/stubbed in §3.1). This story is drafted from those hooks + NOTIF-US-10. Reconcile with the ARCHITECT v0.1.0 pass before dev-start — particularly the switch-state store and the propagation-within-5s guarantee.
Objective¶
v0.1.0 activates a platform-wide kill-switch so Papillon ops can pause and resume all NOTIF dispatch from one endpoint during a provider incident or discovered abuse. While engaged, new dispatch is held in the reserved PENDING_PLATFORM_PAUSED status; in-flight provider submits complete naturally. Resuming releases held sends in FIFO order.
Backend Scope¶
Entities¶
- A single platform-scoped switch state (proposed:
notif_kill_switch (singleton_id, paused BOOL, reason TEXT, engaged_at TIMESTAMPTZ, engaged_by TEXT)— one row; or a Redis flag, see NOTIF-010). Decide in ARCHITECT pass. - Reuses
notification.statusvaluePENDING_PLATFORM_PAUSED(reserved TEXT in v0.0.0, architecture §2.3).
Migrations¶
Vxxx__notif_kill_switch.sql—CREATE TABLE notif_kill_switch (...)with a single enforced row, if DB-backed.
Service Layer¶
KillSwitchGuard.check()evaluated before each dispatch (and before NOTIF-008's cap check). Whenpaused = true, the send is held inPENDING_PLATFORM_PAUSEDand not submitted.- Engage/disengage flips the switch and (on disengage) triggers FIFO release of held sends. Propagation to dispatch workers within 5 s (AC-10.1).
- In-flight provider submits already started are allowed to complete (AC-10.1 "in-flight provider submits complete naturally").
API Endpoints¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| POST | /api/notifications/admin/kill-switch |
{paused: true, reason} / {paused: false} |
200 {paused, engaged_at, engaged_by, reason} |
platform-admin role only |
| GET | /api/notifications/admin/kill-switch |
— | 200 {paused, engaged_at, engaged_by, reason} |
platform-admin role only |
Validation Rules¶
- Only the platform-admin role may call either endpoint → otherwise 403.
reasonrequired whenpaused: true.- Engage is idempotent (engaging an already-engaged switch is a no-op returning current state; does not reset
engaged_at).
Multi-Tenant Considerations¶
- The kill-switch is platform-scoped (all tenants), not per-tenant — it is operated by Papillon ops, above the tenant boundary. This is the one NOTIF control that intentionally crosses all tenants; access is restricted to the platform-admin role (control-plane).
Audit & Logging¶
NOTIF_KILL_SWITCH_ENGAGEDon engage (fields:engaged_by,reason,at).NOTIF_KILL_SWITCH_RELEASEDon disengage.- Both
event_types must be in the AUDIT §B.1 catalog before this story ships (critical rule #3).
Frontend Scope¶
N/A — backend only. The admin UI is V1. AC-10.4 (operator non-dismissible banner "Envoi suspendu plateforme - réessayer plus tard") is a cross-module OP dependency, out of scope for this NOTIF story but noted for the index.
Acceptance Criteria¶
AC-009.1: Engage pauses dispatch within 5s
Given a platform-admin call POST /admin/kill-switch {paused: true, reason}
When the call returns
Then within 5 s no new dispatch is started; in-flight provider submits complete naturally
And new sends are held in PENDING_PLATFORM_PAUSED
And a NOTIF_KILL_SWITCH_ENGAGED audit event is emitted
AC-009.2: Disengage resumes FIFO
Given the kill-switch is engaged with held PENDING_PLATFORM_PAUSED sends
When POST /admin/kill-switch {paused: false} is called
Then held notifications resume in FIFO order
And a NOTIF_KILL_SWITCH_RELEASED audit event is emitted
AC-009.3: Status query and role enforcement
Given the platform-admin role, when GET /admin/kill-switch is called
Then current state, engaged_at, engaged_by, reason are returned
And a non-platform-admin caller to either endpoint receives HTTP 403
Compliance Rules¶
- No direct compliance obligation; operational safeguard (V1 PRD §15.2). ARTCI B5/B10 production BLOQUANT unchanged.
- Kill-switch audit events must be in the AUDIT §B.1 catalog (critical rule #3).
Standards & Conventions¶
docs/standards/critical-rules.md(#3 audit; control-plane role enforcement).docs/standards/multi-tenant-model.md(platform-admin role; platform-scoped control).docs/standards/api-guidelines.md(admin path under/api/notifications/admin; header-based versioning).docs/standards/java-spring-guidelines.md.
Testing Requirements¶
Unit Tests¶
- Engage/disengage idempotency;
reasonrequired on engage. - Role gate: non-platform-admin → 403.
- Held sends transition to
PENDING_PLATFORM_PAUSED.
Integration Tests¶
- Engage → new dispatch held within 5 s; in-flight submit completes.
- Disengage → FIFO release; audit events on both transitions.
- Status query returns engaged_at / engaged_by / reason.
What QA Will Validate¶
- Pausing the platform halts new sends fast, completes in-flight ones, releases in order on resume, and audits both transitions; only platform-admins can operate it.
Out of Scope¶
- Tenant daily cap (NOTIF-008).
- Redis rate-limit + retry (NOTIF-010).
- Admin UI for the switch (V1).
- Operator banner AC-10.4 (OP cross-module, V1).
Definition of Done¶
- [ ]
POST/GET /admin/kill-switchimplemented with platform-admin role gate - [ ] Engage holds new sends in
PENDING_PLATFORM_PAUSEDwithin 5 s; in-flight completes - [ ] Disengage releases FIFO
- [ ]
NOTIF_KILL_SWITCH_ENGAGED/_RELEASEDemitted; present in AUDIT catalog - [ ] All acceptance criteria pass manually
- [ ] Unit + integration tests written and passing
- [ ] Role isolation verified (platform-admin only; 403 otherwise)
- [ ] No silent failures on network errors
- [ ] [ARCH-PENDING] switch-state store + 5 s propagation reconciled with the ARCHITECT v0.1.0 pass before merge to a release branch