DEV Community

Cover image for Dev Log: 2026-08-22 — Driver Seams, Apply-Then-Store, and Credentials That Retire
Nasrul Hazim
Nasrul Hazim

Posted on

Dev Log: 2026-08-22 — Driver Seams, Apply-Then-Store, and Credentials That Retire

TL;DR — A heavy day across a deployment platform and a CRM. The thread running through most of it: the order in which you write things down. Verify before you persist. Sanitize after you interpolate. Record what a machine says, not what you assumed. Here's the log.


1. A managed database server, behind one interface

The biggest chunk of the day. A platform UI that can create databases, users and grants on a node's database daemon — Postgres first, MySQL and MariaDB right behind it.

The whole design rests on a DatabaseAdminContract sitting between the platform and the daemon, with the engine never leaking upward. The interesting part wasn't the method list, it was the promises attached to it: privileges as an engine-agnostic enum instead of raw GRANT passthrough, idempotency documented per method, and a rule that an enum case is a promise a driver exists for it.

Two things I'll repeat because they generalise:

  • Idempotent ≠ permissive. createUser on an existing user sets the password. setPassword on a missing user fails loudly. A repeat of the same intention converges; a different intention still errors.
  • Null means "no evidence". The DTO that carries what a daemon reports about itself keeps port and tlsEnabled nullable, filled from the daemon's own answers. Defaulting them would make "confirmed 5432" indistinguishable from "we guessed".

Also landed: adoption — taking over a daemon the platform didn't install. That's a completely separate, read-only path, because everything the setup pipeline does (move the port, rewrite auth config, open the firewall) is exactly what must not happen to a daemon already serving traffic. It proves reachability, reads the real port and TLS state, inventories what's there, marks the row active. Nothing else. And it cross-checks the recorded port against the daemon's answer, because the client falls back to the default socket — so a wrongly recorded port can still reach a daemon, and you'd happily store an endpoint no client can use.

Full write-up in the companion post today.


2. Apply-then-store, for anything that holds a credential

Password management for a managed cache instance — enable, rotate, disable. The design decision worth stealing is the ordering:

Change the live thing. Verify it. Then persist.

The provisioner changes the running instance and verifies an authenticated ping before returning. Only after that does the action write anything: the stored connection string, the vault entry, and — the easy one to forget — the invalidation of the cached deployment tokens that would otherwise hand the next redeploy the previous password.

The payoff is what happens on failure: a provisioner error leaves every stored credential exactly as it was. Store-then-apply gives you the opposite — a database that confidently describes a state the world isn't in, which is worse than an error, because nothing looks broken.

Two supporting details:

  • The password is generated in the action, not the provisioner. The provisioner's configure() returns void by contract, and the caller has to store — and reveal once — precisely the value that was verified on the wire. Generating it downstream means the value you store is a value you hope matches.
  • No operator-typed passwords. Enable and Rotate both mint a fresh one. A hand-typed value travels through shells and an inline wire protocol on its way to the daemon; a generated alphanumeric never needs quoting. Removing the "set your own" affordance removed an entire class of escaping bug.

And Disable is only honoured when the live instance provably binds loopback. Not "when the config says loopback" — when the running instance does. Config is an intention; the running process is a fact.

enum RedisPasswordOperation: string implements Contract
{
    use InteractsWithEnum;

    case Enable  = 'enable';
    case Disable = 'disable';
    case Rotate  = 'rotate';

    /** Past-tense audit event name, e.g. `redis_password_rotated`. */
    public function auditEvent(): string { /* ... */ }
}
Enter fullscreen mode Exit fullscreen mode

That auditEvent() on the enum is a small thing I keep doing and keep being glad about. The audit string is derived from the operation, in the same file as the operation, so a new case can't ship without one.


3. A handover credential that can retire

Related, and my favourite small idea of the day: some infrastructure providers hand you a one-time credential during provisioning. You need it once. After that it's a liability sitting in your database.

So: surface it in the UI as something the operator can retire with one click — and never clear it automatically. Automatic cleanup sounds tidier, but it means the credential can vanish between the moment you need it and the moment you look for it, and you can't tell "retired deliberately" from "expired quietly". An explicit, visible, one-click retirement is honest about who decided.


4. Monitoring that answers questions

A pile of UI work on the node detail screens, all of it in service of one complaint: the charts looked like charts but didn't say anything.

  • Real y-axis, real tooltips, stat headers on each chart. A sparkline without an axis is decoration.
  • Services listed with which service is which — the deployment's own services in their own labelled card, not two stray rows in someone else's table. Category filters on top (cron, database, cache, and the rest).
  • Both service cards became accordions, collapsed by default. Density is a feature on a page that can list twenty things.
  • The activity tab got search, filters and pagination. Any list that can grow unbounded needs all three eventually; adding them early is cheaper than the migration to a paginated component later.
  • Database service rows now answer managed-or-not with a single link through to the managed server.

None of it is clever. All of it is the difference between a dashboard people check and one they screenshot for a support ticket.

One fix in the same area worth calling out: a password control that lived in a row's overflow menu moved into the opened panel. A destructive-ish action hidden behind a on a collapsed row is an action taken without context.


5. Rich-text email: sanitize twice

Over on the CRM side, the campaign builder. The headline: sanitizing rich-text bodies on save is not enough, because {{variable}} merge tags interpolate contact-supplied text into the document after the save-time pass ran. Sanitize on save, sanitize again on render.

Bonus bug from the same change: a still-uploading attachment serialises as an <img> with no src, and the allow-list strips blob:/data: srcs down to the same shape — which email clients render as a large empty frame. A sanitizer that leaves a husk is worse than one that removes the element. Companion post has the details.

Also shipped there:

  • Reusable audience groups. The campaign audience filter is a plain array stored on the campaign, so it's editable without a deploy. Groups became a third mode alongside attribute filters and hand-picked lists — and like the hand-picked mode, an empty selection matches nobody, not everybody. That default is the difference between a quiet no-op and an accidental send to your entire list.
  • Per-campaign sender identity + CC/BCC. A small readonly DTO with three nullable fields, each falling back to the system sender. Existing records keep behaving exactly as before, which is the whole point of nullable-means-inherit. One nice edge: a From name on its own is still honoured — it gets paired with the system address, because an envelope address can't carry a name without one.
  • Send a test email from the builder, untracked. Testing a campaign shouldn't pollute the campaign's own metrics.
  • A contact 360 redesign — identity header, pipeline stepper, KPI strip — and a timeline that stopped being a wall of gappy plain text. Emails in the timeline now render as the actual email in a flyout, from their real markup.

6. Two deploy-shaped papercuts

Both in the "green deploy, broken app" family:

A stale route cache. Production booted from a cached routes file the pipeline never refreshed. New routes existed in code but not at runtime — and not in artisan processes either, so the deploy operation that depended on the new route failed too. Fixed with a route:clear operation timestamped to run before the one that needed it. If nothing in your pipeline owns invalidating a cached artefact, that artefact will eventually be wrong, and the error message will point anywhere but at the cache.

A storage symlink. Email images served from storage break when public/storage isn't there — a failure that shows up in someone's inbox long after the deploy went green. Serving the asset through an ordinary controller route deletes the precondition entirely. One more route beats one more environmental assumption.

And a genuinely stupid one, recorded for solidarity: a double quote inside an x-data comment broke the whole attribute. Alpine's x-data is an HTML attribute first and JavaScript second, and the HTML parser gets to the quote before the JS parser sees the comment.


7. Refusing instead of guessing

Two small "refuse loudly" changes that belong together:

  • Attaching a workload now picks new-or-existing explicitly, and a name collision refuses instead of silently discarding what you typed. Silently discarding form input is the most infuriating possible failure mode: the user did the work, the app agreed, nothing happened.
  • A native build whose manifest demands a different runtime is now refused, not attempted. Building against the wrong runtime doesn't fail fast — it fails deep, somewhere in a compile step, with an error that has nothing to do with the real cause.

Both are the same principle as the database engine enum from item 1: when you can't serve a request, say so at the boundary with the reason. Half-shipped is fine. Half-shipped and silent is not.


Takeaway

Looking back at the day, almost every good decision was about ordering and honesty:

  • Verify on the wire before you write to the database.
  • Sanitize after the last thing that can add bytes.
  • Record what the machine reported; keep null meaning "nobody knows".
  • Refuse at the boundary, with a reason, instead of guessing and failing deep.

Tomorrow: setup pipelines for the remaining database engines, so adoption stops being the only door in.

Top comments (0)