Documentation / Architecture

Loutre Architecture

Explore the public boundaries that connect Application, Runtime, and Tooling.

Loutre is a TypeScript Application Graph Kernel. Application structure is compiled once into an Application Model; Runtime execution, CLI graph inspection, OpenAPI projection, and deployment tooling all consume that same model.

If you are building your first Application, start with Getting Started.

Overview

Core owns only portable Application concerns:

  • Application Definition and canonical Application Model
  • Module boundaries and Provider / DI
  • Environment and Arguments
  • Lifecycle
  • active execution lifetime
  • Runtime Capability identity
  • generic Layer composition
  • diagnostics and Graph projection

HTTP, Tasks, MessagePort, WebSocket, Queue consumers, and similar execution semantics are owned by Execution Extensions. Core does not branch on protocol identity.

Packages

Public packageRole
@loutrejs/loutreApplication Graph Kernel, DI, lifecycle, runtime abstractions
@loutrejs/nodeNode.js HTTP Runtime Adapter
@loutrejs/bullmqBullMQ Queue Consumer Driver
@loutrejs/cliGraph inspection, validation, build, deployment tooling
create-loutreProject initializer

The main package exposes these important subpaths:

For the rationale behind separating published packages from subpath exports, see the package distribution architecture ADR (Japanese).

SubpathRole
@loutrejs/loutreApplication, Module, DI, lifecycle, Kernel bootstrap
@loutrejs/loutre/httpHTTP Extension
@loutrejs/loutre/tasksTasks Extension
@loutrejs/loutre/message-portMessagePort Extension
@loutrejs/loutre/websocketWebSocket Extension
@loutrejs/loutre/graphApplication Model graph projection
@loutrejs/loutre/runtimeRuntime capability and Kernel runtime primitives
@loutrejs/loutre/http/openapiOpenAPI projection
@loutrejs/loutre/presentationstartup presentation
@loutrejs/loutre/runtime/*portable Runtime adapters

Application Definition and Model

defineApplication() builds the canonical model immediately.

const application = defineApplication({
  modules: [AppModule],
  arguments: AppArgs,
})

An Application Definition does not own a listener, timer, or process. It contains the compiled model, which is the single source of truth used by Runtime and Tooling.

A Module declares Providers and Execution Definitions:

const AppModule = defineModule({
  environment: [AppEnv],
  providers: [UsersService],
  executions: [UsersHttp, heartbeat],
})

Execution Definitions are branded by their owning Extension. During model construction, the Extension compiles each definition into a protocol-neutral execution contribution containing its dependency edges, required Runtime Capabilities, compiled payload, and Graph metadata. An Extension may also expose same-Extension execution references; Core follows that reference closure recursively, so registering a Trigger automatically includes the Task it invokes without retaining raw definitions in the compiled model.

HTTP Extension

HTTP semantics live in @loutrejs/loutre/http.

import { http } from '@loutrejs/loutre/http'
import { z } from 'zod'

export const UsersContract = http.contract({
  get: {
    method: 'GET',
    path: '/users/{id}',
    request: {
      params: { id: z.string() },
    },
    responses: {
      found: {
        status: 200,
        body: z.object({ id: z.string(), name: z.string() }),
      },
    },
  },
})

export const UsersHttp = http.implementation({
  name: 'UsersHttp',
  contract: UsersContract,
  factory: (users = inject(UsersService)) => ({
    async get(ctx) {
      return ctx.response.found({
        body: await users.get(ctx.input.params.id),
      })
    },
  }),
})

HTTP owns route compilation, request decoding, response finalization, middleware composition, authentication helpers, CORS, streaming, and OpenAPI metadata. The Application Model only sees an Execution Extension contribution.

Tasks Extension

Task, Trigger, and Queue Consumer semantics live in @loutrejs/loutre/tasks.

import { fixedDelay, task } from '@loutrejs/loutre/tasks'

export const cleanup = task<void, void>({
  name: 'cleanup',
  factory: () => async () => {
    // work
  },
})

export const heartbeat = fixedDelay({
  name: 'heartbeat',
  delay: 5_000,
  immediate: true,
  task: cleanup,
})

const WorkerModule = defineModule({
  executions: [heartbeat],
})

A hosted Application receives the Extension-owned tasks API only when the model contains Tasks executions.

const app = await bootstrapApplication({ application })
await app.tasks.run(cleanup)
await app.tasks.start()

MessagePort and WebSocket

MessagePort is provided by @loutrejs/loutre/message-port; WebSocket is provided by @loutrejs/loutre/websocket. Both own their execution model, runtime, host API, and graph projection. WebSocket additionally depends on the public HTTP extension surface for its HTTP upgrade handshake; Core remains transport-agnostic.

This keeps transport semantics outside Core while preserving one Application Model.

DI and Module visibility

Providers are application-scoped by default. Dependencies declared with inject() are collected into the Application Model.

class UsersService {
  constructor(readonly repository = inject(UserRepository)) {}
}

Cross-module dependencies require an explicit exports / imports path. Visibility is validated while the model is built, before Runtime startup.

For using dependency-source metadata, see Accessing dependency sources in custom loggers (Japanese). For ownership responsibilities, see the ownership ADR (Japanese).

Environment and Arguments

Runtime inputs are declared as typed contracts and bound before Provider construction.

class AppEnv extends defineEnv(
  z.object({ PORT: z.coerce.number().int().positive() }),
) {}

const AppModule = defineModule({
  environment: [AppEnv],
})

Runtime adapters provide the environment source. bootstrapApplication() can receive an explicit source for tests or embedded execution.

Runtime and lifecycle

bootstrapApplication() creates and initializes the portable Kernel-hosted Application.

const app = await bootstrapApplication({ application })

Runtime adapters add platform ownership around that Kernel application. Node.js, for example, owns the HTTP listener:

const app = await nodeRuntime.create({ application })
await app.serve({ port: 3000 })

Shutdown first drains Extension-owned work and active executions, then closes Extension runtimes, then runs Provider lifecycle cleanup.

Graph and tooling

The CLI loads the Application Definition, reads its compiled model, and projects it without rebuilding a second architecture representation.

loutre check --entry src/app.ts
loutre graph modules --entry src/app.ts
loutre graph di --entry src/app.ts --format mermaid
loutre graph executions --entry src/app.ts --format json
loutre doctor --entry src/app.ts --runtime node

OpenAPI is also an Extension projection. Core does not know HTTP route semantics.

Design invariant

There is one canonical Application Model. Runtime adapters, CLI, build, graph projection, and Execution Extensions must not maintain a second parallel model of the Application.