Skip to content

defineApp

defineApp(name, options) declares a named application — a set of modules + per-app defaults.

Signature

ts
function defineApp(name: string, options: DefineAppOptions): AppDefinition

DefineAppOptions

ts
interface DefineAppOptions {
  modules: readonly ModuleDefinition[]
  description?: string
  defaults?: Omit<CreateAppOptions, "modules">         // runtime defaults

  // Studio-aware
  tenantModel?: "single" | "per-org" | "per-account" | "per-workspace"
  tenantKey?: string                                    // field name in payloads
}

Example

ts
export const learnflowApp = defineApp("learnflow", {
  description: "Adaptive learning loop for AMIT students and teachers.",
  modules: [submissionsModule, enrollmentsModule, masteryModule, lessonsModule],
  tenantModel: "per-org",
  tenantKey: "schoolId",
})

// Same modules, different app composition:
export const lmsApp = defineApp("lms", {
  modules: [enrollmentsModule, rosterModule],
  tenantModel: "per-org",
})

export const lxApp = defineApp("lx", {
  modules: [submissionsModule, lessonsModule],
  tenantModel: "per-org",
})

Instantiate via .create()

ts
const app = learnflowApp.create({
  actorStore: new MongoActorStore(mongoClient.db("nwire")),
  projectionStore: new MongoProjectionStore(mongoClient.db("nwire")),
  bus: new NatsEventBus({ connection: nats }),
  publishToBus: true,
  logger: new PinoLogger(pino()),
  appName: "learnflow-prod",
})

await app.start()

In practice you rarely call .create() directly — the topology composer reads a manifest and instantiates apps with the right provider stack for that deployment shape.

AppDefinition

ts
interface AppDefinition {
  $kind: "app-definition"
  name: string
  description?: string
  modules: readonly ModuleDefinition[]
  defaults: Omit<CreateAppOptions, "modules">
  tenantModel?: TenantModel
  tenantKey?: string
  create(options?: Omit<CreateAppOptions, "modules">): App
}

interface App {
  runtime: Runtime
  modules: readonly ModuleDefinition[]
  start(): Promise<void>          // boots timers / bus subscriptions
  stop(): Promise<void>           // graceful shutdown
}

tenantModel

ValueWhenWhat changes
"single"Single-tenant processStudio hides tenant filters
"per-org"Org-level partition (AMIT school, ecw company)Tenant picker per org
"per-account"B2C per-end-user partitionTenant = user id
"per-workspace"Slack-shape: workspaces inside orgsTwo-level picker

The runtime extracts the tenant id from envelope.tenant regardless of model; tenantModel is metadata for Studio + cloud-routing logic later.

tenantKey

ts
defineApp("learnflow", {
  tenantKey: "schoolId",   // → payloads have schoolId, x-tenant-id header
})

The HTTP wire reads x-tenant-id header into envelope.tenant. If you prefer reading from the body, use a custom envelopeFromRequest on the wire.

Apps vs modules

ModulesApps
Bounded contextDeployment unit
defineModule("submissions", { actions, actors, … })defineApp("learnflow", { modules })
Reused across appsTied to a specific topology slice
The "what"The "where"

See also

MIT licensed.