DEV Community

Cover image for Dev Log: 2026-07-15
Nasrul Hazim
Nasrul Hazim

Posted on

Dev Log: 2026-07-15

TL;DR

  • Shipped account suspension across two Laravel apps — an enum, a middleware, a policy check (own post).
  • Fixed a reverse-proxy gotcha: the proxy hid the gateway's real status behind NGINX's built-in 405.
  • Moved a DB transaction to wrap only the business write, so a rollback stops erasing the audit trail.
  • Added two agent (MCP) tools: read the source-of-truth record directly, and resolve a username to its owner.

Account suspension without deleting the user

Two apps needed the same thing: block a user from signing in without deleting the account. Same shape in both — an enum for the status, a middleware that rejects a suspended session mid-flight, and an admin toggle behind a policy. Its own post today, because the interesting part is the edge cases, not the toggle.

The NGINX 405 that lied

A reverse proxy sits in front of an API gateway. GET worked; anything non-GET came back as a flat 405 Method Not Allowed — but that 405 was NGINX's own, not the gateway's. The proxy was answering for a backend it should have forwarded to. The trap: a limit_except GET (or a stray error_page 405) makes NGINX intercept the method itself instead of passing it through.

Before After
POST/PUT reaches gateway No — NGINX returns its own 405 Yes — forwarded
Status the client sees NGINX built-in 405 The gateway's real status
Debuggability "why is everything 405?" Real upstream errors surface

Lesson: a proxy forwards; it shouldn't have opinions about methods. See a generic 405 on writes? Check the proxy before you blame the backend. I documented it in the config header so the next deploy doesn't reintroduce it.

Scope the transaction around the business write only

Subtle one. An operation did a business write and an audit write inside the same DB::transaction. Looks tidy. But when the business write failed and rolled back, the audit row rolled back with it — the one record meant to survive a failure vanished exactly when you needed it.

// before: audit dies with the thing it was supposed to witness
DB::transaction(function () {
    $this->applyChange($record);   // may throw
    $this->audit->log('change', $record); // rolled back too
});

// after: transaction guards the business write; audit is logged around it
$this->audit->log('change.attempt', $record);
DB::transaction(fn () => $this->applyChange($record));
$this->audit->log('change.done', $record);
Enter fullscreen mode Exit fullscreen mode

Rule of thumb: an audit trail records attempts and outcomes — so it can't live inside the transaction whose failure it's meant to explain.

Two read-only agent tools

Also added two MCP tools over an internal system: one reads the source-of-truth record directly, one resolves a username to its owner across every source it might live in. Both read-only. Treating MCP tools as narrow, read-only lenses — rather than write endpoints — keeps them safe to hand an agent and easy to test.

That's the day: one security feature, one infra gotcha, one durability fix, two agent lenses.

Top comments (0)