[data-reveal]{opacity:1!important;transform:none!important}
Embed AntelopeJS in an Existing App
Guides

Embed AntelopeJS in an Existing App

Run AntelopeJS modules inside an application that owns the process, with AdonisJS 6 as a complete worked example.

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.

AdonisJS 6 is the only host we test. Express, Fastify, NestJS and other Node.js hosts follow the same pattern, but they are untested.

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

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:

antelope.ts
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:

users.ts
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.

To let modules call into the host, list the interface in 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 hookAction
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:

providers/antelope_provider.ts
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:

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:

start/routes.ts
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:

Terminal
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: false through GetRuntimeInfo(). Modules apply their production behavior: for example, @antelopejs/api requires publicBaseUrl.
  • No hot reload — The runtime does not watch module files. Restart the host to pick up a change in a module.
  • No ajs project commands — There is no antelope.config.ts, so ajs project dev, start, build, modules add and logging do not apply. Add a module by installing it and adding it to the modules map. ajs module init and ajs module test still work on the module folders themselves.
  • No configuration file features — Environments, envOverrides, ${path} template substitution and the build artifact belong to antelope.config.ts and do not exist in embedded mode.
  • Installed modules only — Modules come from the host's node_modules or from a local path. 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 with importOverrides; set exportPriority on the modules instead.
  • No module logs by default — Without the logging option, the runtime writes no log lines, so module logs are not printed. Passing logging makes 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