Skip to content

Topology

Deployment shape is what your entry file says. Nwire doesn't ship a "topology manifest" primitive. Each deployable is a small TypeScript file under apps/<name>/main.ts that builds an App (or composes several via appCompose), boots an endpoint(...), and chooses adopters via .use(...). Monolith vs split is a question of how many entry files you write — same domain code either way.

Monolith — one entry, all apps composed

ts
// apps/main/main.ts
import { appCompose } from "@nwire/app"
import { endpoint } from "@nwire/endpoint"
import { httpKoa } from "@nwire/koa"
import { buildLmsApp } from "@amit/lms"
import { buildLxApp } from "@amit/lx"
import { buildCompetencyApp } from "@amit/competency"

const lms = buildLmsApp()
const lx = buildLxApp()
const competency = buildCompetencyApp()

const monolith = appCompose(lms, lx, competency)
await Promise.all([lms.start(), lx.start(), competency.start()])

await endpoint("monolith", { port: Number(process.env.PORT ?? 3000) })
  .use(httpKoa({
    inspect: true,
    openapi: { auto: true, title: "AMIT", version: "1.0.0" },
    docs: true,
  }))
  .mount(monolith)
  .run()

All three apps share one HTTP server and one in-process event bus. The default for nwire dev.

Split — one entry per service

ts
// apps/lms/main.ts
import { endpoint } from "@nwire/endpoint"
import { httpKoa } from "@nwire/koa"
import { natsBus } from "@nwire/nats"
import { buildLmsApp } from "@amit/lms"

const app = buildLmsApp({
  bus: natsBus({ servers: process.env.NATS_URL! }),
})
await app.start()

await endpoint("lms", { port: Number(process.env.PORT ?? 3001) })
  .use(httpKoa())
  .mount(app)
  .run()
ts
// apps/lx/main.ts — same shape, different port + app
import { endpoint } from "@nwire/endpoint"
import { httpKoa } from "@nwire/koa"
import { natsBus } from "@nwire/nats"
import { buildLxApp } from "@amit/lx"

const app = buildLxApp({
  bus: natsBus({ servers: process.env.NATS_URL! }),
})
await app.start()

await endpoint("lx", { port: Number(process.env.PORT ?? 3002) })
  .use(httpKoa())
  .mount(app)
  .run()

Cross-app events go through NATS. Same domain code. Deploy each entry as its own container.

Adding a new deployment

Add one file under apps/<name>/main.ts. Wire it in package.json scripts. No central manifest to update.

jsonc
{
  "scripts": {
    "dev:all":  "vite-node apps/main/main.ts",
    "dev:lms":  "PORT=3001 vite-node apps/lms/main.ts",
    "dev:lx":   "PORT=3002 vite-node apps/lx/main.ts"
  }
}

Provider swapping

The bus, actor store, projection store, logger — everything that ships an adapter — is swapped at the App's plugin layer (or passed to the build factory):

ts
import { MongoClient } from "mongodb"
import { createApp, definePlugin } from "@nwire/app"
import { createMongoActorStore, MongoProjectionStore } from "@nwire/mongo"
import { createPinoLogger } from "@nwire/logger-pino"

const db = (await MongoClient.connect(process.env.MONGO_URL!)).db("orders")

const app = createApp({
  appName: "orders",
  plugins: [
    definePlugin("stores", ({ bind }) => {
      bind("actor.store", createMongoActorStore(db))
      bind("projection.store", new MongoProjectionStore(db.collection("projections")))
    }),
    definePlugin("logger", ({ bind }) => {
      bind("logger", createPinoLogger({ level: "info" }))
    }),
    // ... forgePlugins, etc.
  ],
})

In dev, omit the persistent stores — the InMemory defaults that ship with @nwire/forge take over automatically.

Multi-adopter on one app

Stack adopters on one endpoint to serve the same wires under more transports:

ts
await endpoint("svc", { port: 3000 })
  .use(httpKoa({ prefix: "/api" }))
  .use(queueInMemory())
  .use(mcpAdapter())
  .mount(app)
  .run()

See also

MIT licensed.