---
id: w2/m4/t002
title: Delete verb in the apps feature (App CR + ownerRef cascade; store-row write-through)
worker: worker2
status: done
estimate: 30m
depends_on: []
---

## Objective

Add `Delete` to the apps feature `Service` — the single implementation every adapter will delegate to (grep confirms no delete exists anywhere today). Deleting removes the `App` CR; the operator's ownerRefs cascade the derived Deployment/Service/Ingress. When the control-plane store is enabled (`BEX_CP_DB_URI`), delete writes through the apps row (single-writer-of-intent, per w1/m2) so the projector doesn't resurrect the CR on resync.

## Context

- Feature-package layout: verbs live in `lego/backend/internal/apps/service.go`, authorize-gated first like every verb (`TestAuthzGuardsEveryVerb` must cover it).
- w1/m2's rule: when the store runs, the row owns intent — lifecycle verbs write the row, the projector reconciles the CR. Store-less mode deletes the CR directly (same split as suspend/resume).
- TLS secrets: cert-manager Secrets (`<app>-tls`, `<app>-tls-<host>`) may not be ownerRef'd to the App — check and document what delete leaves behind, or clean up explicitly.

## Steps

1. `Delete(ctx, id)` — authorize, then delete row (store mode; projector removes the CR) or delete the CR directly; `ErrNotFound` for unknown ids.
2. Verify the ownerRef cascade covers Deployment/Service/Ingress; handle the TLS-secret question explicitly.
3. Return semantics matching Render (delete returns no body).

## Files

- `lego/backend/internal/apps/service.go`
- `lego/backend/internal/store/` (delete-row helper if store mode needs one)

## Acceptance criteria

- [ ] `Delete` removes the CR and all operator-derived resources; unknown id → `ErrNotFound`; unauthorized → 403.
- [ ] With `BEX_CP_DB_URI` set, a deleted app stays deleted across a projector resync.

## Out of scope

- Adapter fragments (t003); soft-delete/recovery (Render has none for services).
