Embed AntelopeJS in an Existing App
Overview
This guide runs AntelopeJS in embedded mode: your application owns the process and starts an AntelopeJS runtime as a guest, instead of the ajs CLI owning the process in standalone mode. Read Embedded Runtime for the comparison of both modes and the full API reference.
The steps below apply to any Node.js host. The worked example uses AdonisJS 6, taken from the template-dms-adonis template.
Prerequisites
The host application needs @antelopejs/core 1.5.0 or later, and every module it loads installed as a regular dependency:
npm install @antelopejs/core @antelopejs/mongodb
yarn add @antelopejs/core @antelopejs/mongodb
pnpm add @antelopejs/core @antelopejs/mongodb
The runtime resolves modules from the host's node_modules. It downloads nothing at startup, so a module missing from the dependencies fails start() with an error asking you to install it.
Embed the Runtime
Create the runtime
Call createRuntime() once, during the host's setup phase. List the modules to load in modules, keyed by package name, and the interface packages the host calls in uses:
import { createRuntime } from "@antelopejs/core";
export const runtime = createRuntime({
projectFolder: process.cwd(),
modules: {
"@antelopejs/mongodb": {
config: { url: process.env.MONGODB_URL, database: "app" },
},
},
uses: ["@antelopejs/interface-database"],
});
Module configuration is plain JavaScript. There is no antelope.config.ts, so read environment variables and secrets in host code, with the host's own configuration system.
Start and stop it with the host
Call runtime.start() before the host begins serving requests, and runtime.stop() when the host shuts down. start() rejects when a module fails to start or a uses package does not resolve; let that error stop the host's boot.
The runtime leaves SIGINT, SIGTERM and the process error handlers to the host. If the host never calls stop(), the modules are not stopped or destroyed cleanly.
Reach an interface through runtime.use()
Host code is not a module, so it reaches interfaces through the runtime handle. runtime.use() returns the runtime's own copy of an interface package, or of one of its subpaths, bound to a provider:
import { runtime } from "./antelope";
type DatabaseInterface = typeof import("@antelopejs/interface-database");
const database = runtime.use<DatabaseInterface>("@antelopejs/interface-database");
The request names an interface package, never the module that implements it. The interface must be listed in uses and implemented by a loaded module; use() throws otherwise, and before start().
Never import an interface package from host code
Do not write import { ... } from "@antelopejs/interface-..." in host code. A plain import loads a second copy of the interface package that no provider is attached to: its calls never reach a module. Type-only imports (import type, or typeof import(...) as above) are safe because the compiler erases them.
provides and attach the implementation after start() with runtime.provide(packageName, implementation). Call detach() on the returned handle to remove it.Worked Example: AdonisJS 6
The template-dms-adonis template runs the AntelopeJS DMS inside an AdonisJS 6 application. A service provider ties the runtime to the AdonisJS lifecycle:
| AdonisJS hook | Action |
|---|---|
register() | createRuntime({ ... }), bound into the container. |
start() | runtime.start(), which boots the modules before HTTP serving. |
shutdown() | runtime.stop(), which tears the modules down with the app. |
The service provider
The provider reads its configuration from the validated AdonisJS environment and binds the runtime into the container, so routes and services can resolve it:
import type { ApplicationService } from '@adonisjs/core/types'
import { createRuntime, type AntelopeRuntime } from '@antelopejs/core'
import env from '#start/env'
export const AntelopeRuntimeBinding = 'antelope.runtime'
declare module '@adonisjs/core/types' {
interface ContainerBindings {
[AntelopeRuntimeBinding]: AntelopeRuntime
}
}
export default class AntelopeProvider {
private runtime?: AntelopeRuntime
constructor(protected app: ApplicationService) {}
register() {
this.runtime = createRuntime({
projectFolder: this.app.makePath(),
modules: {
'@antelopejs/dms': {
config: {
homepage: '/modules',
apiBaseUrl: env.get('DMS_API_BASE_URL'),
clientBaseUrl: env.get('DMS_CLIENT_BASE_URL'),
// The embedded runtime never runs in dev mode, so the DMS does not
// publish a per-boot credential for `ajs dms dev`: both read this
// one from `.env`.
frontend: { bootstrapSecret: env.get('DMS_BOOTSTRAP_SECRET') },
meta: {
title: 'AntelopeJS DMS on AdonisJS',
description: 'DMS running as a guest of an AdonisJS application',
},
},
},
'@antelopejs/dms-api': {},
'@antelopejs/dms-media': {},
'@antelopejs/dms-automation': {},
'@antelopejs/dms-database': {},
'@antelopejs/api': {
config: {
// Required outside development, where the runtime runs embedded:
// the origin browsers reach this API at.
publicBaseUrl: env.get('DMS_API_BASE_URL'),
servers: [
{
protocol: 'http',
host: env.get('DMS_API_HOST'),
port: env.get('DMS_API_PORT'),
},
],
cors: {
allowedOrigins: [env.get('DMS_CLIENT_BASE_URL')],
},
},
},
'@antelopejs/mongodb': {
config: {
url: env.get('MONGODB_URL'),
database: env.get('MONGODB_DATABASE'),
},
},
'@antelopejs/auth-jwt': {
config: { secret: env.get('DMS_JWT_SECRET') },
},
'@antelopejs/file-storage-local': {
config: {
storagePath: '.antelope/file-storage',
baseUrl: env.get('DMS_API_BASE_URL'),
defaultVisibility: 'private',
},
},
'@antelopejs/nodemailer': {
config: { ethereal: true },
},
},
uses: ['@antelopejs/interface-dms'],
})
this.app.container.bindValue(AntelopeRuntimeBinding, this.runtime)
}
async start() {
await this.runtime?.start()
}
async shutdown() {
await this.runtime?.stop()
}
}
Register the provider in adonisrc.ts:
providers: [
() => import('@adonisjs/core/providers/app_provider'),
// ...
() => import('#providers/antelope_provider'),
],
Declare every environment variable the provider reads in start/env.ts, so AdonisJS validates them at boot.
Calling an interface from a route
Routes resolve the runtime from the container and call the DMS through runtime.use(). The template types the interface locally, because @antelopejs/interface-dms is a dependency of the DMS module rather than of the application:
import router from '@adonisjs/core/services/router'
import app from '@adonisjs/core/services/app'
import { AntelopeRuntimeBinding } from '#providers/antelope_provider'
type DmsClientBaseUrl = {
GetClientBaseUrl(): Promise<string | undefined>
}
router.get('/dms/info', async () => {
const runtime = await app.container.make(AntelopeRuntimeBinding)
const clientBaseUrl = await runtime
.use<DmsClientBaseUrl>('@antelopejs/interface-dms/client-base-url')
.GetClientBaseUrl()
return { clientBaseUrl }
})
This route listens on the AdonisJS port (3333 by default). The DMS API itself listens on DMS_API_PORT (5010 in the template), as described in the limitations below.
Running the DMS dashboard
The DMS dashboard, @antelopejs/dms-frontend, runs as a separate process. In embedded mode, build it and start it:
pnpm frontend:build # ajs dms build
pnpm frontend:start # ajs dms start -p 3001
ajs dms dev does not work in embedded mode. It hot-reloads the layer sources that the DMS publishes only when the runtime runs in development mode, and the embedded runtime never does. The dashboard presents DMS_BOOTSTRAP_SECRET to the DMS, which checks it against its frontend.bootstrapSecret above; ajs dms build and ajs dms start read DMS_SESSION_SECRET from .env. Run pnpm frontend:build again after you add or remove a DMS module. The DMS AdonisJS page covers the full setup.
Limitations
- Separate HTTP listeners — A module that opens its own HTTP server, such as
@antelopejs/api, listens on its own host and port. It is not mounted into the host's HTTP server, so the host and the module serve on two ports. - Non-development mode — The runtime always reports
dev: falsethroughGetRuntimeInfo(). Modules apply their production behavior: for example,@antelopejs/apirequirespublicBaseUrl. - No hot reload — The runtime does not watch module files. Restart the host to pick up a change in a module.
- No
ajs projectcommands — There is noantelope.config.ts, soajs project dev,start,build,modules addandloggingdo not apply. Add a module by installing it and adding it to themodulesmap.ajs module initandajs module teststill work on the module folders themselves. - No configuration file features — Environments,
envOverrides,${path}template substitution and the build artifact belong toantelope.config.tsand do not exist in embedded mode. - Installed modules only — Modules come from the host's
node_modulesor from a localpath. Package and Git sources are not downloaded. - Default provider for the host —
runtime.use()returns the copy bound to the default provider of each interface. The host cannot pin a provider withimportOverrides; setexportPriorityon the modules instead. - No module logs by default — Without the
loggingoption, the runtime writes no log lines, so module logs are not printed. Passingloggingmakes the runtime take over the AntelopeJS log output and write it next to the host's own logs. - Every module is required — A module that fails to start rejects
start(), and the runtime shuts down the modules it already started.
See also
- Embedded Runtime — Standalone vs embedded, and the API reference
- Interfaces — How interface contracts bind consumers to providers
template-dms-adonis— The complete AdonisJS 6 application