Skip to content

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 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, NodeDurable 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 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.

YOUR CODEYour serviceyour API handlers (product API)app Postgres for non-aggregate statesearch, vectors, lookupsWorker (Go or Node 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-heavyYour 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 → servicesConnect 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 APIYour 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 clientsYour Connect handlers@ironflow/browser for subscriptions and agent invocationThe push channel is decided for you: Ironflow subscriptions over WebSocket or Connect streaming.
Real-time pushSubscriptionsWildcard 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 streamingIronflow 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 eventsEvents and entity streamsEvents 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 jobsFunctions, push or pullRetries, backoff, memoization, resume, dashboard, and circuit breakers included. No second job system.
Scheduled and cron jobsCron 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 sagasDurable steps, wait-for-event, saga compensationThe core mechanism. Single-node SQLite is crash-resume only; high availability needs Postgres plus external NATS.
Human-in-the-loop approvalswaitForEvent, 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 stormsDebounceBuilt in and cluster-safe.
Inbound webhooksWebhook sourcesYour handler as a functionSignature verification, secret rotation, and a delivery log. Outbound webhooks are a function you write.
CLI, standaloneYour language, plainNone of the stack.
CLI that talks to your backendYour generated Connect clientAn Ironflow SDK or generated client for triggering and inspecting runsTwo clients in one binary.
Config, feature flags, secretsConfig, KV, secretsWatchers 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-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 tsvectorNot Ironflow’s job. Search is one reason to keep an application Postgres.
Vector and LLM featuresYour Postgres with pgvectorA function for the embedding pipelineThe pipeline is an event → function, with retries and resume for free.
AI agentsGo or Node 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 toolingironflow 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 languagesPush modeThe Python SDK, or a 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 and Node 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 historyEntity streams, snapshots, upcasters, schema registry, projections, rebuild, time travelBlob overflow to S3 for large snapshotsEntity-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 systemsExternal 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 SaaSOrganization → 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 stressDashboard, 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🟡S3-compatible store, referenced from your dataBlob overflow for large step, run, and snapshot payloadsOverflow is transparent for engine payloads. User uploads still go to S3 directly.
Serverless and FaaS🟡Push modePush 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 systemsMemoized 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 libraryIronflow 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 writesSingle-writer store.
Local development and testsironflow 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, 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.

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 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.