Skip to content
Ironflow is pre-1.0: APIs can change between releases. Pin a version. What is ready →

Stack Fit by Concern

Most teams that reach for event-driven architecture assemble the same three parts: a broker (NATS), a database (Postgres), and an RPC layer (ConnectRPC). The hard part is never any one of them. It is the seams: the outbox between the database and the broker, two RPC styles, two identity systems, and a rule per concern for which component owns queues, locks, schedules, and real-time push.

This page is a fit matrix for teams starting from that baseline. It says, concern by concern, what Ironflow owns, what stays with your own service, and where Ironflow is the wrong tool. It is meant to be able to say no.

Go is the worked baseline here, not a prerequisite. The next section says which of your choices your language actually decides.

Ironflow is a separate process. It is never linked into your binary, including a Go binary: Ironflow embeds NATS JetStream inside itself, but it does not offer itself for embedding in turn. Every engine package is under internal/, closed to external importers, so there is no in-process API to import. You run ironflow serve and talk to it over HTTP and ConnectRPC, the way you run Postgres.

One consequence matters more than the rest: calling Ironflow needs no SDK. The REST API under /api/v1 is published as OpenAPI 3.1, the ironflow CLI works from anything that can start a subprocess, and ironflow mcp exposes the same surface to agents. Emitting events, invoking functions, reading runs, and appending to entity streams are reachable from every stack.

Your language decides one thing: who hosts your function code.

Where the handler runsLanguagesWhat you get
Worker runtimea long-lived process you start, holding an Ironflow SDKGo, Node, PythonDurable steps, sleep, waitForEvent, sagas, agents, and pull mode with no timeout
Push modean HTTP endpoint you already ownanyThe engine POSTs once per invocation. Memoized steps, sleeps, and waits still work, by replay. Bounded by your platform’s request timeout

Nothing else on this page turns on that choice. The rows below are written against a Go and ConnectRPC baseline because that is the stack that assembles a broker, a database, and an RPC layer most often. Read “your Connect handlers” as “your Spring controllers”, “your ASP.NET endpoints”, or “your FastAPI routes” and no row changes.

Ironflow packages a NATS JetStream event bus, a state store (SQLite locally, Postgres in production), and a ConnectRPC gateway in one binary. It does not hand those components to your application:

  • NATS is internal. Your code never opens a NATS connection. Pub/sub, consumer groups, and subscriptions reach you through Ironflow’s API. Raw subject design, the micro services framework, accounts, and leaf nodes are not part of your toolbox.
  • The store is Ironflow’s. It holds runs, steps, entity streams, projections, KV, config, and secrets. Your own relational tables are not in it. Sharing one Postgres server is a deployment choice; sharing Ironflow’s database is not a supported pattern.
  • The gateway is Ironflow’s. Workers, webhooks, the dashboard, and the SDK clients talk Connect and REST to it. Your product’s API is still your own Connect handlers.
  • The worker runtime is Go, Node, or Python. All three SDKs are Tier 1 and peers: memoized steps, sleep, wait-for-event, parallel branches, saga compensation, invoke, and agents run in your process. Every other language reaches the same functions over push mode. See the SDK comparison for the tier model.

The resulting shape is your service, owning its own API handlers and any relational state that is not an aggregate; a worker that runs durable functions, either a Go, Node, or Python SDK worker or a push endpoint in any language; and one Ironflow server between them.

YOUR CODEYour serviceyour API handlers (product API)app Postgres for non-aggregate statesearch, vectors, lookupsWorker (Go, Node or Python SDK)durable functions, steps, waits, sagasagents, projections, consumer groupsor push mode: an HTTP endpoint, any languageIRONFLOW (one binary)ConnectRPC + REST gatewayworkers, webhooks, dashboard, SDK clientsNATS JetStream (internal)events, pub/sub, subscriptions, KVState store (SQLite / Postgres)runs, steps, entity streams, projections,config, secrets, outboxemit events, invoke, append to streamsdispatch runs (push or pull)browsers, mobile, CLIs call your API handlers;the browser SDK subscribes to Ironflow directly for real-time

The three seams of the hand-assembled stack collapse into one question that you must answer per project: is the source of truth an entity stream or a relational table? Most rows below turn on that answer.

✅ strong fit · 🟡 fits with a caveat that matters · ❌ do not force it. “Owner” is the component that should be the default answer.

ConcernFitOwnerSupporting roleCaveat
Web app, CRUD-heavy✅Your Connect handlers + your PostgresIronflow for anything that outlives the requestIronflow adds nothing until there is background work. Do not model a lookup table as an entity stream.
Modular monolith → services✅Connect at the edge, Ironflow functions insideEvents as the module boundaryModules talk by emitting events and invoking functions through the engine. Every cross-module call becomes a recorded run. That is the point, and the overhead.
Public or third-party API✅Your Connect handlersWebhook sources for inboundDo not expose Ironflow’s own API as your product API, even though scoped keys and custom roles make it possible.
Browser and mobile clients✅Your Connect handlers@ironflow/browser for subscriptions and agent invocationThe push channel is decided for you: Ironflow subscriptions over WebSocket or Connect streaming.
Real-time push✅Subscriptions—Wildcard patterns, run and step system events, developer topics. Access is gated by Ironflow’s auth rather than NATS accounts.
Streaming RPC (large results, uploads)✅Your Connect streaming—Ironflow is not in the path.
Service-to-service RPC🟡invoke for commandsDirect Connect for latency-sensitive queriesinvoke goes through the engine: a persisted run dispatched to a worker. It is durable RPC, not sub-millisecond request-reply. Commands via invoke, queries via Connect.
Domain and integration events✅Events and entity streams—Events are stored facts, not signals. Dedup and the outbox are Ironflow’s concern when the write happens in Ironflow. See the next row.
Transactions and invariants✅ / 🟡Entity stream per aggregate, expected-version writesYour Postgres for relational stateAggregate invariants are ✅ through optimistic concurrency. Cross-aggregate invariants are 🟡: a saga, or a table you own. If your Postgres holds the truth and must also emit events, the dual-write problem is yours; Ironflow’s outbox covers only Ironflow’s store.
Background jobs✅Functions, push or pull—Retries, backoff, memoization, resume, dashboard, and circuit breakers included. No second job system.
Scheduled and cron jobs✅Cron triggers on functionsstep.sleepUntil for “at time T”Cron payloads are engine-generated and not schema-validated. A function with both an event and a cron trigger keeps running on schedule when the event trigger is disabled.
Long-running workflows and sagas✅Durable steps, wait-for-event, saga compensation—The core mechanism. Single-node SQLite is crash-resume only; high availability needs Postgres plus external NATS.
Human-in-the-loop approvals✅waitForEvent, or the agent approval gateDashboard for the operatorWaits do not hold a worker. A hosted approval UI is planned, not shipped; today the approval surface is your own UI or the dashboard.
Debounce and event storms✅Debounce—Built in and cluster-safe.
Inbound webhooks✅Webhook sourcesYour handler as a functionSignature verification, secret rotation, and a delivery log. Outbound webhooks are a function you write.
CLI, standalone✅Your language, plain—None of the stack.
CLI that talks to your backend✅Your generated Connect clientAn Ironflow SDK or generated client for triggering and inspecting runsTwo clients in one binary.
Config, feature flags, secrets✅Config, KV, secrets—Watchers in the Go, Node, Python, and browser SDKs; other clients read. KV keeps key history; buckets take a TTL.
Distributed locks, leader election🟡KV create plus compare-and-setAdvisory locks in your PostgresLease-style locks only. Ironflow’s own cluster claim and fencing are internal, not an offered primitive.
Caching🟡KV bucket with TTLYour PostgresNot an LRU cache. Add Redis only when measured.
Full-text search🟡Your Postgres tsvector—Not Ironflow’s job. Search is one reason to keep an application Postgres.
Vector and LLM features✅Your Postgres with pgvectorA function for the embedding pipelineThe pipeline is an event → function, with retries and resume for free.
AI agents✅Go, Node or Python SDK agent() with tool, llm, approve, memory, spawnBrowser SDK to invoke and subscribeSee When Ironflow Fits an Agentic System. Claude SDK and CrewAI adapters are planned, not shipped.
MCP and agent tooling✅ironflow mcp and tools exposed over MCP from the SDKYour own MCP servers on your HTTP transportThe MCP server is read-only unless writes are enabled.
Services in other languages✅Push modeA client generated from the OpenAPI specAny language can host a push function with no SDK; memoized steps, sleeps, and waits work by replay. Pull mode and agents are Go, Node and Python only, by design. Building a pull worker on a generated client is unsupported. See other languages.
Analytics and reporting🟡SQL projections, then exportA warehouse beyond tens of GBSQL projections are real tables in Ironflow’s database. A failing statement is logged, acked, and skipped, and the projection is flagged error rather than stalled.
Event sourcing and history✅Entity streams, snapshots, upcasters, schema registry, projections, rebuild, time travelBlob overflow to S3 for large snapshotsEvents on a live entity stream are always stored. Step execution recording is opt-in, so an inspectable execution history is not a default.
CDC and syncing to other systems✅External projectionConsumer groups on topicsReplaces logical replication when the truth is an entity stream. If your Postgres holds the truth, CDC or an outbox on your side still applies.
High-throughput streaming (over 100k msg/s, TB retention)❌——Every event and step is a store write, and events are retained as facts. Ironflow is not a log. Use Kafka or NATS directly.
IoT, edge, offline🟡Ironflow over HTTP from the edgeBrowser SDK offline write queue for web clientsLeaf nodes and JetStream mirrors are not exposed. Intermittent-connectivity buffering on a Go edge device is yours to write.
Multi-tenant SaaS✅Organization → project → environment, API keys, RBAC, custom roles, policiesYour Postgres RLS for your own tablesTenant = environment. Tenancy is enforced once in Ironflow; your own tables still need RLS.
AuthN and AuthZ🟡Your Connect interceptors for end-user identityIronflow API keys, JWT, and RBAC for service identityTwo identity domains remain: your users and the principals calling Ironflow. Map end-user → scoped key or impersonation deliberately.
Operability under stress✅Dashboard, ironflow inspect, resume from last step, patch step output, replay a stream, rebuild a projection, drain the outbox DLQOTel and PrometheusNothing in the hand-assembled stack offers this without building it.
File and blob storage✅Files: buckets, paths, signed URLs, file eventsBlob overflow for large step, run, and snapshot payloadsFiles live in a filesystem or S3 blob backend. No multipart upload, versions, or per-path access control, and uploads default to 100 MiB. Use S3 directly for larger objects.
Serverless and FaaS🟡Push mode—Push mode is per-invocation HTTP, so a FaaS endpoint can host functions, including memoized steps and waits. Jobs longer than the platform’s request timeout belong on a pull worker.
Exactly-once across systems❌——Memoized step results are exactly-once within a run. That is the only exactly-once claim Ironflow makes. Downstream systems still need idempotency keys.
Ironflow as an in-process library❌——Ironflow embeds NATS; it is not itself embeddable. Every engine package is internal/, so no Go program can import it. It is a server you run beside your app.
Geo-distributed active-active writes❌——Single-writer store.
Local development and tests✅ironflow serve: one binary with SQLite, embedded NATS, and the dashboardYour own test harness for your handlersNo containers for the engine. Multi-node behaviour needs the Postgres path.

Of 39 rows, 26 are ✅, 8 are 🟡, 4 are ❌, and one is both. Six of the caveats share two causes:

  • Kept-Postgres rows (search, cross-aggregate invariants, caching, locks). These exist because Ironflow is not a relational database. A small application Postgres beside Ironflow covers all of them.
  • NATS-exposure rows (RPC latency, edge). These exist because Ironflow does not hand you its NATS. A project that is IoT-first or needs sub-millisecond fan-out RPC should use NATS directly and treat itself as outside this baseline.

The honest cost: in production the stack is Ironflow plus Postgres plus external NATS, three processes again. The difference from the hand-assembled version is that you own none of the seams between them.

These resolve every overlap in the matrix. They are the rules to encode in a project template or an agent skill.

  1. Aggregates live in entity streams. Anything with a lifecycle and invariants is an entity stream with expected-version writes. Relational state that is not an aggregate (lookups, search, vectors) lives in the application Postgres.
  2. A write to your Postgres that must also produce an event is your outbox. Ironflow’s outbox covers only Ironflow’s store. Prefer making the entity stream the write, so the question disappears.
  3. Your Connect handlers are the only thing external clients call. Ironflow’s API is for workers, the dashboard, CLIs, and internal tools.
  4. Cross-service commands go through invoke. Cross-service queries go direct over Connect.
  5. All background, scheduled, delayed, and multi-step work is a function. No second job system.
  6. Real-time to clients is an Ironflow subscription through the browser SDK.
  7. Config, flags, and secrets are Ironflow config, KV, and secrets.
  8. Tenant = environment. Thread the environment ID through your own tables as the tenant key.
  9. Standalone CLIs use none of it. CLIs that talk to a backend carry the generated Connect client plus the SDK client.

These are expensive to change later.

  • Truth model per project. Entity-stream-first, or Postgres-first with Ironflow for orchestration only. This decides whether rule 2 ever fires.
  • One Postgres server or two. Ironflow’s database and the application database on one server as two databases, or separate servers. Never one database.
  • Read-model default. SQL projections inside Ironflow (queryable, rebuildable), or external projections writing into your Postgres (better joins with relational data). Pick one default; the other is the exception.
  • Identity mapping. How an end user in your Connect layer becomes a scoped Ironflow principal: a per-tenant key, impersonation, or a service key carrying tenant context.
  • Push or pull. Pull is the safer default, and it needs a Go, Node or Python worker: a long-lived process, no timeout. Push is for stateless short handlers, and it is the only mode in every other language.