Writing Indie SaaS ops

Raw DELETE dies at the edge

When the browser’s HTTP method dies at the hosting edge, the delete that worked locally never meets your ACL or your toast.

Local delete returns 200. Same route on the public host returns a serif Forbidden page. The row is still there. Your ACL never ran. The toast never fired.

In this note, the edge is the shared-hosting gate in front of the app (web server plus ModSecurity-class rules). It is not your middleware and not your route ACL.

Wrong model: Local green on a raw verb means production will accept that verb, and a serif Forbidden page means your app ACL said no.
Actual model: The edge can strip the method before the app boots. Forbidden HTML is an edge shape. An ACL deny is usually a redirect plus toast.

Raw verb means the browser sends a real DELETE / PUT / PATCH. Tunneled verb means the browser sends POST plus a body field naming the intended verb; the app remaps after the edge. In this fixture, only the tunnel clears the gate and hits the delete handler. The raw verb stops with Forbidden before the app runs.

Invariant: a mutating action is production-ready only when the method that crosses the public edge is one that the hop allows. Framework route verbs the edge strips never run.

Local delete, production Forbidden

On a laptop there is no shared-hosting edge. A fetch with method: "DELETE" reaches the handler and returns 200. That green path is real for local. It is false confidence for production.

On the public host the same request never reaches the app: status 403, Forbidden HTML, handler hits stay at zero, row still present. If you read that page as “permission denied,” you dig ACL and session while the delete route is idle.

Edge gate is not app ACL

App ACL deny, in this fixture, is a 302 with an access_denied toast. Different status, different body shape, different owner. Confusing the two wastes the first hour of the incident.

The production-ready method is the one that survives the edge, not the one your framework documents for the route. Laravel-style method override (POST + _method) is one tunnel. It is not magic; it is a remapping that happens after a hop the edge already accepted.

Lab: raw verb vs tunneled verb

I ran node lab/raw-delete-dies-at-the-edge.mjs in this repo on 8 September 2026. In-process edge + app. No network.

Assertions the fixture encodes:

  • Local raw DELETE returns 200 (false confidence for production).
  • Edge raw DELETE returns 403, reachedApp: false, handler never runs, row still present.
  • .htaccess would have allowed DELETE (htaccessWouldAllow: true); the edge still blocks.
  • Tunneled POST + _method=delete reaches the app, remaps to DELETE, deletes row-1.
  • App ACL deny is redirect + toast, not Forbidden HTML.
  • Same tunnel pattern works for PUT.

Minimal edge and remap:

// Edge: only GET and POST reach the app.
if (method !== "GET" && method !== "POST") {
  return { status: 403, reachedApp: false, blockedBy: "edge" };
}

// App: POST + _method remaps after the edge.
if (method === "POST" && body?._method) {
  method = String(body._method).toUpperCase(); // DELETE, PUT, …
}

Trimmed report:

{
  "localRawDelete": { "status": 200, "ok": true, "falseConfidence": true },
  "edgeRawDelete": {
    "status": 403,
    "reachedApp": false,
    "blockedBy": "edge",
    "htaccessWouldAllow": true,
    "handlerHits": 0,
    "rowStillPresent": true
  },
  "tunneledDelete": {
    "status": 200,
    "reachedApp": true,
    "effectiveMethod": "DELETE",
    "spoofed": true,
    "deleted": "row-1"
  },
  "appAclDeny": {
    "status": 302,
    "shape": "app_toast_redirect",
    "toast": "access_denied"
  },
  "tunneledPut": {
    "status": 200,
    "effectiveMethod": "PUT",
    "label": "renamed"
  },
  "allPassed": true
}

Local green and production Forbidden can both be true. The interesting failure is that the delete handler never ran.

Rejected: allowlisting DELETE in .htaccess alone

I rejected fixing this with only <Limit DELETE> (or similar) in .htaccess. In the fixture the allowlist would have said yes and the edge still returned Forbidden. The gate sits above that file.

Tunneling is the wrong tool when a true non-browser client must emit raw DELETE and the host allows it, or when the real fix is ModSecurity policy with ops, not a form spoof. Same family as other local≠production readiness bugs: the check that mattered was the hop in front of the app, not the route table you tested without that hop.

What the tunnel does not buy

A tunneled verb does not make raw-verb API clients work. It does not justify blanketing ModSecurity off. It does not remove CSRF from forms. It does not prove every host blocks these verbs. It does not fix ACL bugs that already return app deny UX.

The method that crosses the public edge is the method that counts. Local 200 on a raw verb is not that proof.

A public notebook

Notes from real delivery: racey quotas, sync when a device is offline, messaging APIs, and the gap between a clean local demo and production.

Not a product catalog, a tutorial syllabus, or a course funnel. If a post names a tool, I used it. If it describes a failure, it happened.

About Contact