惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

D
DataBreaches.Net
V
Vulnerabilities – Threatpost
C
CERT Recently Published Vulnerability Notes
Google DeepMind News
Google DeepMind News
GbyAI
GbyAI
Y
Y Combinator Blog
T
Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
MyScale Blog
MyScale Blog
Security Latest
Security Latest
T
Threat Research - Cisco Blogs
量子位
I
Intezer
Simon Willison's Weblog
Simon Willison's Weblog
C
Cybersecurity and Infrastructure Security Agency CISA
L
Lohrmann on Cybersecurity
L
LINUX DO - 最新话题
The Register - Security
The Register - Security
T
Tailwind CSS Blog
爱范儿
爱范儿
Google DeepMind News
Google DeepMind News
T
Troy Hunt's Blog
Stack Overflow Blog
Stack Overflow Blog
Cloudbric
Cloudbric
S
Secure Thoughts
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss
L
LangChain Blog
Recorded Future
Recorded Future
小众软件
小众软件
www.infosecurity-magazine.com
www.infosecurity-magazine.com
T
Tor Project blog
人人都是产品经理
人人都是产品经理
F
Full Disclosure
O
OpenAI News
Webroot Blog
Webroot Blog
A
Arctic Wolf
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog
Jina AI
Jina AI
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
雷峰网
雷峰网
Microsoft Security Blog
Microsoft Security Blog
H
Heimdal Security Blog
B
Blog RSS Feed
Vercel News
Vercel News

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Conduit: The Gateway I Built to Forget About
Adam - The Developer · 2026-06-24 · via DEV Community

Three microservices, two incompatible auth models, one public endpoint, and a Go gateway that turned into a pile of strings.HasPrefix conditionals. Here's the rewrite, in Go + TypeScript, and why it ended up sandboxed across two runtimes.

The article today might be a little boring depending on how much you're into systems and distributed architecture. And if you're already thinking, "Great, another API gateway," I can't really blame you.

But bear with me for a bit: Conduit isn't just another request-forwarding proxy. Along the way, we'll dive into programmable gateways, sandboxed runtimes, Unix sockets, and some of the engineering decisions that made building it surprisingly fun.


TL;DR

  • Built a small Go API gateway in front of three services with two different auth schemes (custom HMAC sessions, and JWT).
  • It worked. It also became unmaintainable: every routing exception meant editing Go and redeploying.
  • Rewrote it as Conduit: Go stays on the network path (routing, proxying, CORS, SSRF guards). A sandboxed Deno runtime runs policy as plain TypeScript plugins, talking to Go over a Unix socket with a strict snapshot-and-patch protocol.
  • Net result: auth, logging, and routing rules became files I could edit and save, no recompiling the gateway.
  • Full FAQ/defense-of-design-decisions Q&A is here, not in this post.

The setup

Three services, one public API:

  • chat-service: real-time messaging
  • user-service: profiles, sessions, identity
  • admin-service: internal tooling, elevated operations

User and chat traffic ran a custom session + HMAC scheme: Authorization: Session <id>, signed with X-Signature, X-Timestamp, X-Nonce, backed by Redis for sessions and replay protection. Not JWT. Not OAuth. Something built and iterated on for months.

Admin traffic was JWT, verified at the edge, then forwarded with trusted X-Gateway-Admin-* headers so the admin service skipped re-validation.

Different trust models. Different header shapes. Different public-path exceptions. All of it had to land on one hostname, because clients don't care how many services you run behind it.

So I built the obvious thing: a small Go gateway. Service-prefixed paths picked the upstream. A route.Select() function picked the auth mode. A growing stack of prefix conditionals decided which paths were public:

/admin-service/*  → admin upstream   + JWT (except login, refresh, external-api)
/user-service/*   → user upstream    + HMAC (except auth, device, internal, …)
/chat-service/*   → chat upstream    + HMAC for users, JWT for /api/v1/admin/*

It shipped. It worked. It was ugly.

Where the ugliness actually lived

Not in reliability. Requests hit the right upstream, JWT/HMAC validation worked, sessions slid TTL correctly. The ugliness was in extensibility:

res := route.Select(r.URL.Path)
proxy := proxies[res.Backend]
h, ok := applyAuth(w, res.Auth, cfg, sessionStore, nonceStore, proxy)
h.ServeHTTP(w, r)

Clean on paper. But adminAuth(), userAuth(), and chatAuth() were each a wall of strings.HasPrefix branches, each one carrying tribal knowledge about which paths were exceptions. Every new public-path exception meant editing Go and redeploying the gateway. Figuring out why a path behaved a certain way meant reading three functions and cross-referencing a README the size of a small service.

I wasn't failing at microservices. I was failing at boundary discipline: policy had nowhere clean to live except deeper inside the gateway's Go source.

Every new exception made the gateway more specific instead of more general.

That gateway didn't get thrown away. It proved the routing model was right. It just proved the implementation model needed to change. That became Conduit.

Design goals (the actual constraints, not aspirations)

1. Freedom of implementation. If you can write TypeScript, you can extend the gateway. No DSL, no Lua config, no plugin marketplace format: drop a file in ./plugins, export an object with lifecycle hooks:

import type { GatewayContext } from "../runtime/shared/types.ts";

export default {
  beforeRequest(ctx: GatewayContext): void {
    if (!ctx.request?.headers?.Authorization) {
      ctx.reject!(401, "missing token");
    }
  },
};

2. Low operational overhead. No control plane, no Kubernetes requirement, no sidecar ceremony. Default setup is one Go binary supervising a Deno runtime, reading a JSON config:

go run ./cmd/conduit -config conduit.config.json

3. Explicit separation of concerns.

Layer Responsibility
Go gateway HTTP ingress, routing, upstream proxying, CORS, body limits, timeouts, SSRF guards
Deno runtime Sandboxed plugin execution in a warm worker pool
Plugins Business policy: auth, logging, transforms, route control

Go never gets to know about plugin internals. The runtime never owns routing. Plugins never touch the network boundary directly. These invariants are written down because violating them produces cross-language bugs that tests don't reliably catch.

4. Predictable failure modes. A misbehaving plugin shouldn't take down the gateway. Hook timeouts log and continue. Worker crashes replace the isolate, not the process. If the entire runtime is unreachable, failPolicy decides: closed (503, no proxy) or open (skip plugins, proxy anyway), configurable per route.

5. Invisible in production. The bar I actually cared about: deploy a plugin when policy changes, glance at structured logs when something's off, otherwise forget the thing exists.

Architecture: two processes, one socket

flowchart LR
    C[Client]
    G[Go Gateway<br/>Routing, Proxying, CORS]
    D[Deno Runtime<br/>Policy Plugins]
    U[Upstream Services]

    C --> G
    G <-- Unix Socket --> D
    G --> U

Per request:

  1. Client hits the Go gateway.
  2. CORS preflight (OPTIONS) is answered in Go; plugins never see it.
  3. Go builds a serializable context snapshot from the request.
  4. Every plugin's beforeRequest hook runs, in filename order.
  5. Go proxies once to the matched (or plugin-selected) upstream.
  6. The upstream response gets attached to context.
  7. Every plugin's afterResponse hook runs, same order.
  8. Go writes the response to the client.

Upstream services have zero awareness Conduit exists: no SDK, no self-registration. Multi-service routing is config, not code:

{
  "routes": [
    { "path": "/user-service/*",  "upstream": "http://user-service:2001" },
    { "path": "/admin-service/*", "upstream": "http://admin-service:2002" },
    { "path": "/chat-service/*",  "upstream": "http://chat-service:2003" }
  ]
}

One plugin chain, many backends, policy applied uniformly at the edge.

The actual hard part: getting context across the process boundary

The real problem with the first gateway wasn't routing logic. It was where state lived. Mutable request objects passed through middleware created hidden coupling, and that gets worse the moment you're crossing a process boundary: you cannot hand Deno a live http.Request and hope for the best.

Conduit's contract:

  1. Go snapshots the request into serializable JSON.
  2. The snapshot crosses a length-prefixed frame over a Unix socket to Deno.
  3. The plugin operates on a cloned, hook-local context with a frozen inbound request.
  4. The plugin returns a patch: a minimal delta, not a mutated object.
sequenceDiagram
    participant G as Go
    participant D as Deno

    G->>D: HookInput { ctx }
    D->>D: deepClone(ctx)
    D->>D: run plugin hook
    D->>D: computePatch(baseline, mutated)
    D-->>G: HookOutput { ctxPatch }
    G->>G: ApplyPatch(ctx, patch)

Why patch instead of full context on return: sending the complete context back from every hook means re-serializing headers, identity, and potentially large bodies, twice per request, per plugin. The patch engine instead sends only what changed: omitted fields are unchanged, present fields overwrite, null tombstones a deletion. A logging plugin that sets one state field ships a few dozen bytes back, not the whole request graph.

Why bodies never fully cross the wire: anything above 64 KiB stays in a per-request BodyStore in Go. Plugins get a stream:// reference token, not raw bytes. Megabytes stay in Go; metadata crosses the socket.

Plugins don't own the request. They propose changes to it.

That sentence is the whole design. Reject/forward signals, the patch diffing rules, the frozen snapshot: all of it exists to make "propose, don't own" safe and cheap.

The plugin model

Plugins stay deliberately small:

Hook When
onLoad Once at startup
beforeRequest Before upstream proxying
afterResponse After upstream response is captured
onError When a hook in this plugin throws unexpectedly

ctx.reject() is intentional control flow, not an error; onError is for actual surprises.

Plugins load in filename order, full stop:

plugins/
  001-auth.ts           ← runs first
  002-logging.ts        ← sees ctx.user from auth
  003-header-rewrite.ts
  004-route-control.ts

No dependency graph, no hidden priority system. 001-auth.ts runs before 002-logging.ts because of the filename, and that's grep-able at 3am.

The ctx surface is narrow on purpose:

Member Role
ctx.request Read-only inbound snapshot
ctx.response Outbound builder (setStatus, setHeader, setBody)
ctx.user Identity set by auth plugins
ctx.state Per-request key/value bag shared across hooks
ctx.log Structured logging with trace correlation
ctx.reject(status, msg) Stop pipeline, return HTTP error
ctx.forward(url) Proxy to an alternate upstream (validated by the gateway)
ctx.services Optional http and cache helpers

No global singletons, no ambient mutable request object. Auth sets ctx.user; logging reads it. That's the entire coordination contract.

What happened when I actually ran it

The first deployment was personal: three services behind one port, HMAC checks in one plugin file, JWT checks in another, logging in a third. The prefix-matching spaghetti became three files I could read top to bottom.

Then it spread quietly: internal tools needing a stable hostname, admin surfaces needing stricter gating, experimental routes I could add without redeploying backends.

The moment I knew it worked wasn't a benchmark. It was forgetting it was running, then stopping the Docker container to debug something unrelated and watching everything break. That's the success criterion I actually cared about: not feature parity with Kong, invisibility under normal operation.

Failure modes, stated plainly

Event Behavior
Plugin hook exceeds timeout (default 100ms) Warning logged; request continues proxying
Plugin worker crashes Isolate replaced; gateway process survives
Entire Deno runtime unavailable failPolicy: "closed" → 503; "open" → proxy without plugins
ctx.reject(401) Pipeline stops; remaining hooks skipped
Invalid ctx.forward() target Logged, ignored; original route used
Upstream failure Standard 502 propagated

Hook timeout and runtime death are different failure classes on purpose. A slow logger shouldn't look like a dead security layer.

The riskiest failure mode isn't a crash, though. It's silent semantic change: a plugin that mis-sets ctx.user or swallows a header keeps the system at 200 OK while behavior quietly drifts underneath it. That's why every log line carries [trace:<id>], and why reject and thrown errors are treated as distinct signals.

What it's not

  • Not Envoy: no xDS, no WASM filter ecosystem at scale.
  • Not Kong: no admin UI, no plugin marketplace.
  • Not a service mesh.
  • Not for multi-gigabyte streaming responses. afterResponse buffers the body so plugins can inspect/rewrite it, which costs memory proportional to response size.

What it gains in exchange: a codebase readable in an afternoon, plugins you edit and save instead of recompile, no cloud dependency to define policy, and bounded IPC cost (one warm Unix socket, patches instead of full payloads, body references instead of raw bytes).

It's lean, not free. Four plugins across two phases is up to eight round trips per request. Sub-millisecond on a local socket. Wrong tool if you need per-byte stream processing at scale; right tool if you've got a handful of services, evolving edge policy, and a team that already writes TypeScript.

What stuck

  • Abstraction is only good if it disappears in daily use. The pieces that survived are the ones I stopped thinking about. The pieces that died were baked into Go: the adminAuth()/userAuth()/chatAuth() prefix ladders that worked fine until they needed to change without a redeploy.
  • Cross-process plugin systems are viable if state transfer is strict. Patches, body references, frozen snapshots. Not optimization trivia; the actual precondition for Go and Deno cooperating without shared-memory bugs.
  • Failure design shaped operability more than any plugin API did. failPolicy, hook timeouts, and the reject/onError split mattered more than feature count.
  • "Simple" means boundary discipline, not low line count. Go owns the network. Deno owns execution. Plugins own policy proposals. Upstream owns business logic. Nobody reaches across.

Closing

This wasn't built to be a product. It was a response to a system that stopped fitting in my head: three services, two auth models, one endpoint, and a gateway that grew a new strings.HasPrefix branch every time something shipped.

The goal was never to out-build Envoy or Kong. It was to build something I could forget existed until the day I needed to reason about it. At that point, reasoning about it should be easy: read the plugin files, read the config, follow one request through Go → IPC → Deno → upstream → back.

If you're standing where I was (auth diverging, routing logic leaking into every handler), you might not need another service. You might need a place where policy can live without infecting everything else.


Try it:

go run ./cmd/conduit -config conduit.config.json

export default {
  beforeRequest(ctx) { /* policy */ },
  afterResponse(ctx) { /* transforms */ },
};

Full design-decision Q&A (why Deno over Node/Lua/WASM, scaling, "when is this the wrong choice," etc.) is in my site, not here.