Part 3 in the series on building the Handrail AI agent platform. Start from the beginning: Why AI Agents in the SDLC.
In the previous post we decided: one platform core, and every idea becomes a pluggable workflow. This post lays out exactly what that core is made of, what contract a new workflow must satisfy, and how we split the knowledge base — because that part isn’t obvious, and the technical decision here had real security consequences.
The shape of the core
Events arrive from three kinds of sources: Jira/GitHub Issues, monitoring systems, and customer tickets. They all land at one Ingestion Gateway, which deduplicates and routes them into a Graph Runtime (LangGraph.js) backed by a Checkpointer on Postgres. The runtime dispatches into whichever of the four workflows — Clarify, Patch, Learn, or Triage — the event belongs to. Every workflow talks to external tools only through a Tool Gateway, sends approval requests through a shared Approval Service, and has its LLM output checked by a shared Guardrail Service. Every one of those three — the gateway, the approval service, the guardrail — writes to one central Audit Log. Learn owns and reads/writes a customer-support knowledge base; Clarify, Patch, and Triage read and write a separate, internal engineering knowledge base.
Core components, one sentence each
- Ingestion Gateway — one set of webhook endpoints, shared deduplication (the same problem shows up in all four workflows: a ticket, an alert, a customer request, a CI build — only the key differs).
- Graph Runtime (LangGraph.js) — one instance hosting many graphs, each workflow its own
StateGraph. - Checkpointer (Postgres) — one state table for every workflow, enabling a single „what’s waiting for review” view instead of four separate ones.
- Tool Gateway — the only point through which any workflow touches external tools (today: the repository, via MCP); access policies live here, enforced centrally. Deliberately does not control access to the knowledge base — that’s the knowledge store’s retrieval layer’s job, to avoid mixing two responsibilities in one component. Honest caveat: gateways like this are a young infrastructure category, and this is the least-proven component of the whole design — at the start, policies are enforced by code convention, not a real gateway.
- Approval & Notification Service — shared human-in-the-loop primitives („send for approval,” „escalate after no response”), different channels per workflow.
- Guardrail Service — shared validation of LLM output (hallucinations, prompt injection) before showing it to a human.
- Audit Log — one immutable record of every agent proposal and human decision, across the whole platform.
The workflow contract
Every new workflow (four today, more tomorrow) provides a single module declaring: its event types, its graph, how to deduplicate it, its approval spec, its guardrails, its tool-access scopes, and its knowledge-base access. Adding a fifth idea means a new file implementing this contract, not a change to the core.
The knowledge base: shared infrastructure, but two namespaces — not one
This is one of the few decisions here we made for security reasons, not convenience. Knowledge from the Learn workflow (answering customers) and knowledge from the Patch/Triage workflows (internal standards, bug patterns) are two different sensitivity domains:
- The customer-support knowledge base — can reach a customer directly as an autonomous reply at any moment.
- The engineering knowledge base — architectural standards, bug patterns, sometimes security details.
If this were one flat vector collection, an autonomous reply to a customer could land on an internal article about an unpatched vulnerability and send it automatically, with no review at all. Hence the hard split, enforced in the knowledge store’s retrieval layer: the workflow that answers customers has no technical ability to query the engineering knowledge base.
Trade-off: two collections instead of one means more configuration (separate namespaces, separate access policies) and less „accidental” sharing of knowledge across workflows. We pay that price because the alternative — one shared base — is a failure category that’s hard to catch after the fact (internal knowledge leaking to a customer) and very costly when it happens.
Technology choices — shown with alternatives, not just the verdict
A caveat for every number in this section: they come from the comparison authors’ synthetic benchmarks (links in the bibliography), run on their hardware against their workloads — treat them as orders of magnitude for comparing options, not guarantees. In real scenarios with a database in the path, the differences between HTTP frameworks flatten to negligible.
HTTP layer: we chose Fastify, not Hono. Hono has better raw benchmark performance (78,200 req/s vs. Fastify’s 62,400 req/s) and native TypeScript with type inference without extra annotations — but it’s also built for multi-runtime (Bun, Deno, edge). Since the rest of the platform stays on Node, that advantage doesn’t matter to us, and Fastify has the more mature ecosystem in plain Node with schema-first validation.
Event queue: we actually walked a real ladder of options, not just one answer:
- BullMQ (Redis) — the simplest in Node, retry/backoff out of the box, but yet another separate infrastructure component alongside Postgres.
- RabbitMQ — a mature, general-purpose broker (10K-100K msg/s), dead-letter exchanges for retry, a solid choice if it’s already in the organization.
- NATS JetStream / Kafka — NATS: 1-2M msg/s with persistence, very low operational overhead; Kafka: 1M+ msg/s and a durable, replayable event log, but decidedly heavier operationally (partitioning, KRaft/ZooKeeper).
- Ultimately: Graphile Worker — a queue native to Postgres, 1K-10K jobs/s. It won not because it’s the fastest (it isn’t), but because it eliminates a separate infrastructure component and solves the „dual write” problem: enqueuing a job and writing graph state can happen in one SQL transaction. At our volume (webhooks from Jira/monitoring, not millions of events per second), throughput isn’t the constraint, and transactionality is a real correctness win.
Vector database: pgvector on the same Postgres, not Qdrant right away. The 2026 benchmark numbers: at 99% recall, pgvector+pgvectorscale hits an order of magnitude more throughput than Qdrant and keeps latency under 100ms even in parallel — but Qdrant only pulls ahead above 10-50M vectors, or when metadata filtering needs to happen inside the search index. At our scale (two knowledge-base namespaces, realistically thousands, not millions, of articles), pgvector wins on simplicity — zero new infrastructure, embeddings and documents in the same table.
Deployment: Docker Compose to start, migrating to Kubernetes only once a concrete reason appears (autoscaling against queue depth, multi-region, or an org-wide k8s standard). Two stateless containers — an API service and a worker — talking to each other only through Postgres, which means a later migration is, in practice, a deployment-config rewrite, with no change to the application architecture.
The weakest leg of this architecture — named outright
Honesty requires pointing at where this design is least certain. It’s not the database or the queue — it’s the durability mechanism for long-running workflows. LangGraph’s checkpointer saves state only between graph nodes, not inside a node: on resume, the interrupted node re-executes from its beginning, so any side effect placed before a pause point in the same node runs a second time. Not a theory — we found exactly this bug in our own code during review (questions posted to Jira twice on resume). On top of that, the Postgres checkpointer package we use sits at a pre-1.0 version, with a real risk of breaking changes.
The mature alternative is Temporal — a durable-execution engine with event-sourced replay that solves this class of problems systemically; the pattern described as mature in 2026 is „Temporal for macro-orchestration + LangGraph for micro-level agent reasoning.”
Trade-off: we’re staying with plain LangGraph — simplicity and zero new infrastructure, at the cost of systemic durable-execution guarantees we don’t need at MVP scale. But the decision has named validity limits: a growing number of suspended workflows, side effects too costly to run twice, or breaking changes in pre-1.0 packages would all be reasons to revisit it.
What’s next
The core is designed. But before we build it in full, the next post does something counterintuitive: it picks the first application (Handrail Clarify) and deliberately defers building the full core for later — because laying foundations before any first working result is exactly the risk we named earlier in this series.
Bibliography
- Pgvector vs. Qdrant (TigerData)
- pgvector vs Qdrant: PostgreSQL Extension or Dedicated Vector Database (Encore)
- NestJS vs Fastify vs Hono: The 2026 Node.js Comparison (Encore)
- Message Brokers Comparison 2026 — Kafka, RabbitMQ, NATS & Redis Streams
- Postgres as a Queue: When You Don’t Need Kafka or RabbitMQ
- Graphile Worker (GitHub)
- Durable execution — LangGraph docs — the checkpointer’s node-boundary limitation
- LangGraph vs Temporal (LangChain)