I've been trying to keep my publishing schedule to one article a week, but this one felt too special to wait so I had Claude helping me generate a ...
For further actions, you may consider blocking this person and/or reporting abuse
I really like this approach! And yes, authorization is one of those things that quickly turns into a nightmare when it's scattered across multiple microservices. 😅
Having a single place where authentication and authorization policies live makes the system so much easier to reason about and evolve. The plugin-based approach is especially neat because you can change policy without touching the gateway itself.
Very cool project!
Thank you!
That's pretty much the problem Conduit came from. The auth wasn't hard, but the exceptions kept piling up and slowly turning the gateway into a mess of special cases.
The plugin system was really just my way of giving policy its own place to live so it could evolve without constantly changing the gateway itself.
I'm glad you enjoyed it!
Since plugins run in filename order and each hook is a round trip over the socket, that chain sets your latency floor per request, right? Four plugins across two phases is the eight round trips you mention, all sequential. I'm curious whether you considered letting independent plugins in the same phase run at the same time, or whether the ordering (002-logging reading the
ctx.userthat 001-auth set) matters often enough that keeping it serial is just the honest call. The "propose, don't own" patch model is the part that'll stick with me. It's the cleanest answer I've seen to sharing request state across a process boundary without the usual mutation bugs.Yes sirrr, each hook is a sequential socket round trip, so cost scales with plugin count. Serial order is intentional when plugins depend on each other (auth → logging).
Conduit won't stop you adding more files, but it won't hide the cost either. If chains keep growing, maybe I'd consider performance work there.
...probably batching a phase into one IPC trip before parallel merge.
Glad the patch model landed!
The three discipline primitives that survive in this design (propose-don't-own, patches not mutations, filename ordering not dependency graphs) keep showing up in adjacent territory under different vocabularies. The same shape lands in operator-side decision audit work, where the surface authoring policy must not own the artifact it gates and write-side fixes only work if read-side cooperates. Different domain, identical architectural commitment: the gate authority cannot live inside the thing being gated.
The silent-semantic-change failure mode is the part I think the post under-credits relative to how much work it does. A plugin mis-setting ctx.user without throwing keeps the system at 200 OK while behavior drifts, and trace:id alone tells you the request happened, not that the meaning held. The one extension I would push on, less for Conduit specifically and more for the shape: a periodic planted-fault test against the patch engine itself. Submit a patch that should be rejected (a logging plugin trying to set ctx.user, or an auth plugin trying to overwrite a header marked immutable), watch whether the rejection fires or the patch lands silently. The patch engine is only doing the work it claims if it has demonstrably caught a deliberate violation in recent memory. Same shape as the failPolicy calibration: closed only matters if you have evidence it ever closed.
Honest stage marker on this side: I work adjacent (verification engineering on dev.to, no API gateway in production), and the design choice that lands hardest reading the architecture section is the boundary discipline as primitive, not the plugin model. The plugin model is the surface that makes the discipline survive contact with daily edits. That distinction is the part most plugin-system writeups skip.
Oh wow, thank you.
The distinction you drew between boundary discipline as the primitive and the plugin model as the surface that keeps it alive is exactly what I was reaching for in the architecture section, but you articulated it far better than I did. The "gate authority cannot live inside the thing being gated" framing is especially good—I may end up borrowing that in a future revision.
I think you're right about silent semantic drift, too. A crash is obvious. A timeout is visible. A mis-set
ctx.userthat still returns200 OKis much harder to detect because nothing appears broken from the outside.The planted-fault idea is also a really interesting extension. Conduit already verifies that request snapshots remain frozen on the plugin side, but it doesn't yet enforce ownership boundaries at patch-merge time. Today,
ApplyPatchmostly trusts convention: auth ownsctx.user, logging reads it. Turning that into something the system actively proves—rather than something contributors are expected to respect—is a direction worth exploring.I appreciate the thoughtful read. The fact that the boundary-discipline angle stood out more than the gateway itself probably means that part of the design came through after all.
The "ApplyPatch mostly trusts convention" line is the part I want to take in trade. It names the failure mode that boundary-discipline systems usually leave undocumented because convention feels like architecture from the inside until somebody breaks it. The path forward you described (turning ownership into something the system actively proves rather than something contributors respect) is exactly the boundary discipline as primitive move you already shipped at the IPC layer, applied one floor down to plugin-vs-plugin write coordination.
One concrete shape that has been holding up for me in adjacent work: declarative ownership per plugin (auth.ts declares OWNS ctx.user, logging.ts declares READS-ONLY ctx.user) plus a patch-merge gate that validates write targets against declarations rather than trusting filename ordering. Same IAM-least-privilege pattern at the plugin layer, with the same planted-fault test as the IPC contract: submit a patch from logging.ts that sets ctx.user, watch whether the gate fires or the patch lands. If the gate ever stops firing for a real reason (somebody legitimately broadened logging.ts ownership), the declaration changes are the audit trail, not the conventions in the readme.
Vocabulary trade in return: "boundary discipline as primitive" only works if the primitive itself has a planted-fault test that demonstrably caught a deliberate violation in recent memory. Otherwise the primitive is also in the convention bucket, just at a higher abstraction. Same trap, longer label.
I agree with the trade.
“Convention until proven otherwise” is the line I should have led with. The plan is exactly that: declarative writes per plugin, Deno as the sole merge gate, planted faults where logging attempts to set
ctx.user, and a declaration diff that serves as the audit trail. Your standard for primitives is fair—until that check is enforced in CI, boundary discipline is still just aspiration at the plugin coordination layer.I’ll take that as the ship criterion.
Thanks again for engaging on this.
Ship criterion locked in public counts for something, because now the failure mode if you don't ship it is also visible. The trade I'd flag forward is on the planted-fault piece specifically: the catch only counts if the failure shape is declared before the planted attempt runs, otherwise you can retrofit "that's actually what we caught" after the fact and the test passes for the wrong reason.
Pre-commit declaration of what "logging attempts to set ctx.user" looks like at the gate, what evidence the gate produces when it fires, and what the failure mode is if the gate silently passes. Three records, not one. Same shape as the rest of Conduit's contracts.
Glad to be on the other side of this one when the planted-fault run is live.
right, and agreed on the bar.
that planted-fault only counts if the expected failure shape is declared before the run. Otherwise it’s too easy to retrofit interpretation after the fact.
I’ll run it with a pre-commit declaration in the three records you called out:
002-loggingattemptsctx.userwrite),If the output doesn’t match that declaration, it fails. No post-hoc fitting.
Hey, this article appears to have been generated with the assistance of ChatGPT or possibly some other AI tool.
We allow our community members to use AI assistance when writing articles as long as they abide by our guidelines. Please review the guidelines and edit your post to add a disclaimer.
Failure to follow these guidelines could result in DEV admin lowering the score of your post, making it less visible to the rest of the community. Or, if upon review we find this post to be particularly harmful, we may decide to unpublish it completely.
We hope you understand and take care to follow our guidelines going forward!
done.