- Comparisons & Licensing
- Stack Fit by Concern
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 server, not a library
Section titled “Ironflow is a server, not a library”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 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 runs | Languages | What you get | |
|---|---|---|---|
| Worker runtime | a long-lived process you start, holding an Ironflow SDK | Go, Node | Durable steps, sleep, waitForEvent, sagas, agents, and pull mode with no timeout |
| Push mode | an HTTP endpoint you already own | any | The 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.
What changes when Ironflow is the default
Section titled “What changes when Ironflow is the default”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
microservices 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 or Node. Both 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 or Node SDK worker or a push endpoint in any language; and one Ironflow server between them.
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.
The matrix
Section titled “The matrix”✅ strong fit · 🟡 fits with a caveat that matters · ❌ do not force it. “Owner” is the component that should be the default answer.
| Concern | Fit | Owner | Supporting role | Caveat |
|---|---|---|---|---|
| Web app, CRUD-heavy | ✅ | Your Connect handlers + your Postgres | Ironflow for anything that outlives the request | Ironflow 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 inside | Events as the module boundary | Modules 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 handlers | Webhook sources for inbound | Do 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 invocation | The 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 commands | Direct Connect for latency-sensitive queries | invoke 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 writes | Your Postgres for relational state | Aggregate 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 functions | step.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 gate | Dashboard for the operator | Waits 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 sources | Your handler as a function | Signature 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 client | An Ironflow SDK or generated client for triggering and inspecting runs | Two clients in one binary. |
| Config, feature flags, secrets | ✅ | Config, KV, secrets | — | Watchers in the Go, Node, and browser SDKs; other clients read. KV keeps key history; buckets take a TTL. |
| Distributed locks, leader election | 🟡 | KV create plus compare-and-set | Advisory locks in your Postgres | Lease-style locks only. Ironflow’s own cluster claim and fencing are internal, not an offered primitive. |
| Caching | 🟡 | KV bucket with TTL | Your Postgres | Not 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 pgvector | A function for the embedding pipeline | The pipeline is an event → function, with retries and resume for free. |
| AI agents | ✅ | Go or Node SDK agent() with tool, llm, approve, memory, spawn | Browser SDK to invoke and subscribe | See 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 SDK | Your own MCP servers on your HTTP transport | The MCP server is read-only unless writes are enabled. |
| Services in other languages | ✅ | Push mode | The Python SDK, or a client generated from the OpenAPI spec | Any language can host a push function with no SDK; memoized steps, sleeps, and waits work by replay. Pull mode and agents are Go and Node only, by design. Building a pull worker on a generated client is unsupported. See other languages. |
| Analytics and reporting | 🟡 | SQL projections, then export | A warehouse beyond tens of GB | SQL 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 travel | Blob overflow to S3 for large snapshots | Entity-stream events 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 projection | Consumer groups on topics | Replaces 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 edge | Browser SDK offline write queue for web clients | Leaf 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, policies | Your Postgres RLS for your own tables | Tenant = environment. Tenancy is enforced once in Ironflow; your own tables still need RLS. |
| AuthN and AuthZ | 🟡 | Your Connect interceptors for end-user identity | Ironflow API keys, JWT, and RBAC for service identity | Two 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 DLQ | OTel and Prometheus | Nothing in the hand-assembled stack offers this without building it. |
| File and blob storage | 🟡 | S3-compatible store, referenced from your data | Blob overflow for large step, run, and snapshot payloads | Overflow is transparent for engine payloads. User uploads still go to S3 directly. |
| 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 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 dashboard | Your own test harness for your handlers | No containers for the engine. Multi-node behaviour needs the Postgres path. |
Reading the matrix
Section titled “Reading the matrix”Of 39 rows, 25 are ✅, 9 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.
Ownership rules
Section titled “Ownership rules”These resolve every overlap in the matrix. They are the rules to encode in a project template or an agent skill.
- 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.
- 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.
- Your Connect handlers are the only thing external clients call. Ironflow’s API is for workers, the dashboard, CLIs, and internal tools.
- Cross-service commands go through
invoke. Cross-service queries go direct over Connect. - All background, scheduled, delayed, and multi-step work is a function. No second job system.
- Real-time to clients is an Ironflow subscription through the browser SDK.
- Config, flags, and secrets are Ironflow config, KV, and secrets.
- Tenant = environment. Thread the environment ID through your own tables as the tenant key.
- Standalone CLIs use none of it. CLIs that talk to a backend carry the generated Connect client plus the SDK client.
Decisions to make first
Section titled “Decisions to make first”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 or Node worker: a long-lived process, no timeout. Push is for stateless short handlers, and it is the only mode in every other language.
- Comparison with Temporal, Inngest, Hatchet, Restate, Kurrent, and Kafka
- Architecture for what sits inside the binary
- SDK comparison for what each language gets
- Deploying alongside your app