[data-reveal]{opacity:1!important;transform:none!important}
Concepts

Configuration

Configure your Antelopejs project with antelope.config.ts — define modules, sources, environment overrides, logging, and per-environment settings.

Configuration File

Antelopejs uses antelope.config.ts as its configuration file. The framework expects a TypeScript config file at the root of your project, providing full type safety and autocompletion through the defineConfig helper.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig({
  name: "my-project",
  modules: {
    // Module definitions
  },
});

The defineConfig helper provides full type checking for your configuration. Place the configuration file in the root of your project directory.

Dynamic Configuration

The config file can export a function instead of a static object. The function receives a context object with the current environment, allowing you to vary settings based on deployment target.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig((ctx) => ({
  name: "my-project",
  modules: {
    "my-module": {
      source: { type: "local", path: "./modules/my-module" },
      config: {
        apiUrl:
          ctx.env === "production"
            ? "https://api.prod.com"
            : "http://localhost:3000",
      },
    },
  },
}));

Full Configuration Reference

The top-level configuration object accepts the following fields. All fields except name are optional.

interface AntelopeConfig {
  name: string; // Project name
  cacheFolder?: string; // Default: ".antelope/cache"
  modules?: Record<string, string | AntelopeModuleConfig>; // Module definitions
  logging?: AntelopeLogging; // Logging configuration
  envOverrides?: Record<string, string | string[]>; // Environment variable mappings
  environments?: Record<string, Partial<AntelopeConfig>>; // Per-environment overrides
  test?: AntelopeTestConfig; // Test configuration
}

Module Definitions

Define modules as a simple version string (shorthand for an npm package) or as a full configuration object with source, config, and overrides.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig({
  name: "my-project",
  modules: {
    // Shorthand: npm package with version
    "my-module": "1.0.0",

    // Full configuration
    "my-other-module": {
      source: { type: "local", path: "./modules/my-module" },
      config: { port: 3000 },
      importOverrides: { "@antelopejs/interface-database": "mongodb-module" },
      disabledExports: ["@antelopejs/interface-legacy"],
    },
  },
});

The shorthand format "my-module": "1.0.0" is equivalent to specifying a package source with that version. Use the full object form when you need local sources, custom config, import overrides, or disabled exports.

Module Source Types

Each module source type tells the core where to find the module code and how to load it. The following table summarizes the available types.

TypeDescription
localLocal filesystem path to a TypeScript/JavaScript project.
local-folderLocal folder loaded directly without compilation.
packagenpm package fetched from a registry.
gitGit repository cloned and built.

Local Source

Point to a module on your local filesystem. The core watches the specified directories for changes during development.

{
  type: "local",
  path: "./modules/my-module",      // Relative path to the module
  main: "custom-entry.js",          // Custom entry point (optional)
  watchDir: ["src", "lib"],         // Directories to watch (optional, string or string[])
  installCommand: "npm run build",  // Command to run after install (optional, string or string[])
  reloadCommand: "npm run build"    // Command to run on hot reload instead of installCommand (optional, string or string[])
}

When set, reloadCommand runs in place of installCommand during a hot reload, letting you configure a faster command (for example, a build that skips dependency installation).

Package Source

Fetch a module from an npm registry. The core downloads and caches the package automatically.

{
  type: "package",
  package: "@scope/my-module",
  version: "1.0.0"
}

The version field accepts an exact version, an npm semver range (^1.0.0, ~1.2.0, 1.x), or a dist-tag (latest, next). Exact versions are served from the cache without touching the registry. Ranges and dist-tags are resolved against the registry on every run, so the cache is refreshed automatically whenever a newer matching version is published. If the registry is unreachable, a cached version that still satisfies the range is used as a fallback.

Git Source

Clone a module from a Git repository. You can pin to a specific branch or commit.

{
  type: "git",
  remote: "https://github.com/user/repo.git",
  branch: "main",                                        // optional
  commit: "abc123",                                      // optional
  installCommand: "npm install && npm run build"          // optional
}

Local Folder Source

Load a module directly from a folder without any compilation step. This source type is useful for pre-built modules or JavaScript-only projects.

{
  type: "local-folder",
  path: "./modules/my-module",
  watchDir: "src",                  // optional
  installCommand: "npm install",    // optional
  reloadCommand: "npm run build"    // runs instead of installCommand on hot reload (optional)
}

Interface Bindings

When several modules implement the same interface, each module that uses it is bound to one of them. The binding is fixed when the module imports the interface: the core evaluates the interface once per provider, and everything built from that copy (a query, a schema, an event handler registration) reaches that provider wherever it is used.

Pinning a provider

importOverrides pins an interface to a provider module:

antelope.config.ts
{
  importOverrides: {
    "@antelopejs/interface-database": "mongodb-module"
  }
}

A module may pin interfaces it does not import itself. A pin is carried down: through the standalone interfaces the module uses (a data-api built on the pinned database), and into the modules that provide the interfaces it uses, as far as some module below still needs the interface. Pinning the top-level module of a tree is usually enough.

For each interface, a module is bound, nearest first, to itself when it implements it, to its own pin, to the pin carried from its nearest pinned ancestor, or to the default provider.

Default provider

With a single provider, that provider is the default. With several, the default is the provider with the highest exportPriority for the interface, and the alphabetically first one on a tie:

antelope.config.ts
{
  modules: {
    "mongodb-module": {
      exportPriority: { "@antelopejs/interface-database": 1 },
    },
  },
}

A positive priority marks the main provider; a negative one marks a provider that should never become the default by accident. When a module is bound by default while several providers tie, the core logs a warning naming the module and the top-level module where one pin would settle it.

Conflicts

A module reached through two paths that carry different bindings for an interface it needs, either two different pins, or a pin on one side and the default on the other, stops startup with an error naming that module. Pinning the interface on the module the error names settles it.

Connections

Further importOverrides entries for the same interface, each with an id, give a module several connections:

antelope.config.ts
{
  importOverrides: [
    { interface: "@antelopejs/interface-database", source: "mongodb-module", id: "main" },
    { interface: "@antelopejs/interface-database", source: "postgres-module", id: "external" },
    { interface: "@antelopejs/interface-data-api", id: "external" },
  ],
}

The first entry is the pin a plain import reaches. GetInterfaceInstance(interfaceName, id).path names the copy of the interface bound to that connection; require() it (or one of its subpaths) to build objects bound to that provider. An entry without a source declares a connection to a standalone interface: here, the data-api built on the external database.

Disabled Exports

Prevent a module from providing specific interfaces. The module still loads and runs, but the core does not register the listed interfaces from it.

antelope.config.ts
{
  disabledExports: ["@antelopejs/interface-legacy"];
}

Environment Variable Overrides

The envOverrides field maps environment variables to configuration paths. When an environment variable is set, its value overrides the corresponding configuration value at runtime. The path uses dot notation to target nested fields.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig({
  name: "my-project",
  modules: {
    "db-module": {
      config: { host: "localhost", port: 5432 },
    },
  },
  envOverrides: {
    DB_HOST: "modules.db-module.config.host",
    DB_PORT: "modules.db-module.config.port",
  },
});

With this configuration, setting DB_HOST=db.prod.com in your environment overrides the host value in the db-module config. Environment variable overrides keep sensitive or deployment-specific values out of your configuration file.

Template Substitution

Use ${path} syntax in string configuration values to reference other values within the same config block. The core resolves these references before passing the config to the module.

antelope.config.ts
{
  config: {
    host: "localhost",
    port: 5432,
    url: "${host}:${port}/mydb"  // Resolves to "localhost:5432/mydb"
  }
}

References of the form ${@<module>.<VAR_NAME>} belong to the reserved config variables namespace: they are left untouched by this pass and resolved later, once their provider module published them.

Module Config Variables

A value that only exists at run time — the port a server actually bound, a generated secret — cannot be written in the configuration file. A module can instead publish it as a config variable, and the other modules reference it from their own config.

Declaring and publishing

The provider declares the names it publishes in its package.json (or antelope.module.json), under antelopeJs.configVars:

package.json
{
  "name": "api",
  "antelopeJs": {
    "configVars": ["API_PORT"]
  }
}

It then returns the values from its provide callback, a lifecycle step that runs before every module constructs:

index.ts
import type { ConfigVars } from "@antelopejs/interface-core/config";

export async function provide(config: unknown): Promise<ConfigVars> {
  const port = await reservePort(config);
  return { API_PORT: port };
}

provide is optional and only providers implement it. construct cannot publish config variables: whatever it returns is ignored.

A provider must compute its values on its own. Nothing has constructed when provide runs, so awaiting another module's interface from it deadlocks the startup — that is a user error, not a supported pattern. The core interfaces registered before the module lifecycle starts, GetRuntimeInfo() among them, stay available and are enough to derive a port, a base URL or a secret. A provider may still read a variable published by another provider, through its own config: those few provider-to-provider links are ordered for you.

Referencing

A consumer references a published value with ${@<module>.<VAR_NAME>}, where <module> is the key of the provider in modules:

antelope.config.ts
export default defineConfig({
  name: "my-project",
  modules: {
    api: { source: { type: "local", path: "./modules/api" } },
    dms: {
      source: { type: "local", path: "./modules/dms" },
      config: {
        apiBaseUrl: "http://127.0.0.1:${@api.API_PORT}", // "http://127.0.0.1:5010"
        servers: [{ port: "${@api.API_PORT}" }], // 5010, as a number
      },
    },
  },
});

A reference that makes up the whole string keeps the published type (a number stays a number); a reference embedded in a longer string is interpolated. Substitution reaches any depth of the configuration, arrays included, and leaves every other value exactly as it was — a RegExp, a Date or a class instance in the config comes out untouched.

Resolution order

The core reads every declaration before running anything, so the provider/consumer graph is known upfront:

  1. Providers publish their variables in the provide phase. Providers that read no variable publish first and concurrently; a provider reading from another one waits for it.
  2. The published values are substituted into the config of every module that references them.
  3. Every module then constructs, all at once, exactly as it would without config variables.

Construction itself is never ordered by the variable graph. It must not be: the interface graph orders modules the other way round — a module serving an interface its peers call from their own construct has to construct alongside them — and staging construction on the variables would make the two orders wait on each other.

Values are resolved once and frozen: a value that changes after startup is not propagated.

Failure modes

All of these are hard errors, raised with the module and the reference named:

SituationWhen
A reference to a module that is not loadedBefore anything runs
A reference to a variable the provider does not declareBefore anything runs
A cycle in the graph (api -> dms -> api)Before anything runs
A declared variable the provide callback did not returnAfter the provider published
A provider whose provide failedIts consumers are reported as failed and are never constructed with an unresolved config

An unresolved reference never becomes an empty string.

A module skipped because its provider failed still owes the interfaces it declared. Its peers do not read config variables from it — they call it — so the async proxies of those interfaces reject instead of waiting, naming the module that never constructed and the interface. A boot that ends in failures reports them and leaves with a non-zero exit code, even when a module bound a socket before its peer threw.

Environment-Specific Configuration

The environments field defines per-environment overrides. Each key is an environment name, and its value is a partial configuration that merges on top of the base config when that environment is active.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig({
  name: "my-project",
  modules: {
    "api-module": {
      config: { port: 3000 },
    },
  },
  environments: {
    production: {
      modules: {
        "api-module": {
          config: { port: 8080 },
        },
      },
    },
  },
});

Run your project with a specific environment using the -e flag, or set ANTELOPEJS_LAUNCH_ENV. When the configuration declares environments, the flag accepts default and the names defined there: ajs project build, ajs project modules and ajs project logging check the name before doing any work, and an unknown name stops the command with exit code 2 and lists the known environments. When the name comes from ANTELOPEJS_LAUNCH_ENV, the error says so. A configuration that declares no environments, static or exported as a function, accepts any name and uses its base configuration, since the configuration itself handles the environment (through ctx.env or process.env).

ajs project dev -e production

The base configuration applies first, then the environment-specific overrides merge on top. The environment block overrides only the fields you specify — everything else keeps its base value.

Logging Configuration

Configure log output directly in the config file. The logging section controls which channels appear, their minimum severity level, and the output format.

antelope.config.ts
{
  logging: {
    enabled: true,
    moduleTracking: {
      enabled: true,
      includes: ["my-module"],    // Only show logs from these modules
      excludes: ["noisy-module"]  // Hide logs from these modules
    },
    channelFilter: {
      "main": "info",             // Level name or number
      "http": 10,                 // DEBUG level
      "db:*": "warn"              // Prefix wildcard
    },
    dateFormat: "yyyy-MM-dd HH:mm:ss"
  }
}
See the Logging page for a full explanation of severity levels, channels, and filtering options.

Complete configuration example

The following example combines all configuration features into a single realistic file. Each field includes an inline comment explaining its role.

antelope.config.ts
import { defineConfig } from "@antelopejs/interface-core/config";

export default defineConfig({
  // Project name — identifies this project in logs and CLI output
  name: "my-fullstack-app",

  // Directory for build artifacts and cached modules (default: ".antelope/cache")
  cacheFolder: ".antelope/cache",

  modules: {
    // Shorthand: npm package source — version string resolves to { type: "package", version }
    "@antelopejs/auth-jwt": "^1.0.0",

    // Local source: point to a module on disk, with a watch directory for live reload
    "api-module": {
      source: {
        type: "local",
        path: "./modules/api",
        watchDir: ["src"],
        installCommand: "npm install",
      },
      config: {
        port: 3000,
        host: "localhost",
        url: "${host}:${port}", // template substitution — resolves to "localhost:3000"
      },
      importOverrides: {
        "@antelopejs/interface-database": "mongodb-module",
      },
      disabledExports: ["@antelopejs/interface-legacy-api"],
    },

    // Git source: clone a module from a remote repository
    "analytics-module": {
      source: {
        type: "git",
        remote: "https://github.com/your-org/analytics-module.git",
        branch: "main",
        installCommand: "npm install && npm run build",
      },
      config: {
        trackingId: "UA-000000-1",
      },
    },

    // Package source (explicit form): fetch from an npm registry
    "mongodb-module": {
      source: {
        type: "package",
        package: "@antelopejs/mongodb",
        version: "^1.2.0",
      },
      config: {
        url: "mongodb://localhost:27017",
        database: "app_dev",
      },
    },
  },

  // Map environment variables to configuration paths using dot notation
  envOverrides: {
    DB_URL: "modules.mongodb-module.config.url",
    DB_NAME: "modules.mongodb-module.config.database",
    // A single env var can override multiple paths
    API_HOST: [
      "modules.api-module.config.host",
      "modules.analytics-module.config.endpoint",
    ],
  },

  // Per-environment overrides — merged on top of the base config
  environments: {
    production: {
      modules: {
        "api-module": {
          config: { port: 8080, host: "0.0.0.0" },
        },
        "mongodb-module": {
          config: {
            url: "mongodb://db.prod.internal:27017",
            database: "app_prod",
          },
        },
      },
      logging: {
        enabled: true,
        channelFilter: { default: "warn" },
      },
    },
    staging: {
      modules: {
        "mongodb-module": {
          config: {
            url: "mongodb://db.staging.internal:27017",
            database: "app_staging",
          },
        },
      },
    },
  },

  // Logging configuration — controls console output for all modules
  logging: {
    enabled: true,
    moduleTracking: {
      enabled: true,
      includes: ["api-module", "mongodb-module"],
      excludes: [],
    },
    channelFilter: {
      main: "info",
      http: 10,
    },
    dateFormat: "yyyy-MM-dd HH:mm:ss",
  },
});

Key takeaways

  • Shorthand modules accept a version string and resolve to the package source type automatically.
  • envOverrides keep secrets and deployment-specific values out of the config file. Set DB_URL in your shell or CI environment and the value flows into the correct module config.
  • environments let you maintain a single config file for development, staging, and production. Only the fields you specify in an environment block are overridden.
  • Template substitution (${host}:${port}) references other values in the same config block, so you can build derived strings without duplication.
  • Config variables (${@api.API_PORT}) reference a value another module publishes from its provide callback, before anything constructs, so a runtime value lives in one place instead of being duplicated across module configs.

See also