defineApp
defineApp(name, options) declares a named application — a set of modules + per-app defaults.
Signature
ts
function defineApp(name: string, options: DefineAppOptions): AppDefinitionDefineAppOptions
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
| Value | When | What changes |
|---|---|---|
"single" | Single-tenant process | Studio hides tenant filters |
"per-org" | Org-level partition (AMIT school, ecw company) | Tenant picker per org |
"per-account" | B2C per-end-user partition | Tenant = user id |
"per-workspace" | Slack-shape: workspaces inside orgs | Two-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
| Modules | Apps |
|---|---|
| Bounded context | Deployment unit |
defineModule("submissions", { actions, actors, … }) | defineApp("learnflow", { modules }) |
| Reused across apps | Tied to a specific topology slice |
| The "what" | The "where" |