Colony Documentation

Complete documentation for Colony, the environment-aware config loader for Node.js.

Installation

npm install @ant.sh/colony

Requirements: Node.js 18+

Optional peer dependencies:

  • @aws-sdk/client-secrets-manager - For AWS Secrets Manager integration

Core Concepts

What is a Dimension?

A dimension is an axis along which your configuration varies. Think of it as a question your config needs to answer:

  • "Which environment am I running in?" → env dimension (dev, staging, prod)
  • "Which region am I deployed to?" → region dimension (us, eu, asia)
  • "Which customer is this for?" → tenant dimension (acme, globex, initech)

The key insight: Instead of creating separate config files for each combination (config-dev.json, config-prod.json, config-prod-eu.json...), you define dimensions and write rules that apply to specific combinations.

Declaring Dimensions

Use @dims at the top of your config file:

@dims env;                    # Single dimension
@dims env, region;            # Two dimensions
@dims env, region, tenant;    # Three dimensions

The order matters - it determines how you write your rules.

How Dimensions Work: A Complete Example

Let's say you're building a SaaS app deployed across environments and regions:

@dims env, region;

# Default for ALL environments and ALL regions
*.*.database.port = 5432;
*.*.database.pool.size = 10;
*.*.api.timeout = 5000;

# Defaults for development (any region)
dev.*.database.host = "localhost";
dev.*.api.url = "http://localhost:3000";

# Defaults for production (any region)
prod.*.database.host = "prod-db.internal";
prod.*.database.pool.size = 50;
prod.*.api.timeout = 10000;

# Region-specific overrides
prod.us.database.host = "prod-db-us.internal";
prod.us.api.url = "https://api-us.myapp.com";

prod.eu.database.host = "prod-db-eu.internal";
prod.eu.api.url = "https://api-eu.myapp.com";

Now in your code:

// Development
const devConfig = await loadColony({
  entry: "./config/app.colony",
  ctx: { env: "dev", region: "us" }
});
// database.host → "localhost"
// database.pool.size → 10

// Production US
const prodUsConfig = await loadColony({
  entry: "./config/app.colony",
  ctx: { env: "prod", region: "us" }
});
// database.host → "prod-db-us.internal"
// database.pool.size → 50

Wildcards (*)

The * wildcard matches any value for that dimension:

@dims env, region;

*.*. ...        # Matches any env, any region (default)
prod.*. ...     # Matches prod env, any region
*.us. ...       # Matches any env, us region
prod.us. ...    # Matches only prod env AND us region

Specificity: Which Rule Wins?

When multiple rules match, Colony picks the most specific one.

Specificity = count of non-wildcard dimension values.

@dims env, region;

*.*.timeout = 1000;           # specificity: 0 (two wildcards)
prod.*.timeout = 2000;        # specificity: 1 (one wildcard)
prod.us.timeout = 3000;       # specificity: 2 (no wildcards)

For ctx: { env: "prod", region: "us" }:

  • All three rules match
  • prod.us.timeout = 3000 wins (highest specificity)

Tie-breaker: If two rules have the same specificity, the one defined later wins.

Config File Syntax

File Extension

Colony config files use the .colony extension.

Comments

# Line comment (hash)
// Line comment (double slash)

/* Block comment */

/*
  Multi-line
  block comment
*/

Directives

Directives start with @ and configure the parser:

@dims env, region;              # Declare dimensions
@include "./base.colony";       # Include another file
@include "./envs/*.colony";     # Include with glob pattern
@require database.host;         # Require key to be set
@envDefaults env=dev;           # Default context values

Operators

Operator Name Description
=SetSet value, overwrites existing
:=Set if missingSet only if key doesn't exist
|=MergeDeep merge objects, overwrite primitives
+=AppendAppend to array (creates array if needed)
-=RemoveRemove value from array

Examples:

# Set (overwrites)
*.timeout = 5000;

# Set if missing (won't overwrite)
*.timeout := 3000;

# Merge objects
*.database |= { pool: { min: 5 } };

# Append to array
*.features += "dark-mode";

# Remove from array
prod.features -= "debug-panel";

Values

Colony uses JSON5 for values, supporting:

# Strings
*.name = "MyApp";
*.name = 'MyApp';                    # single quotes ok

# Numbers
*.port = 5432;
*.ratio = 0.75;

# Booleans
*.enabled = true;
*.debug = false;

# Null
*.optional = null;

# Arrays
*.hosts = ["a.com", "b.com"];

# Objects
*.database = {
  host: "localhost",
  port: 5432,
  ssl: true
};

# Trailing commas allowed
*.list = [1, 2, 3,];

Heredoc Strings

For multi-line strings:

*.template = <<<EOF
Hello, ${name}!
Welcome to our service.
EOF;

Interpolation

Environment Variables:

*.api_key = "${ENV:API_KEY}";
*.home = "${ENV:HOME}";

Context Values:

*.endpoint = "https://api.${ctx.region}.example.com";

Custom Variables:

*.data_path = "${VAR:ROOT}/data";

JavaScript API

loadColony(options)

Load and resolve a colony configuration.

import { loadColony } from "@ant.sh/colony";

const config = await loadColony({
  // Required
  entry: "./config/app.colony",

  // Optional
  ctx: { env: "prod", region: "us" },
  dims: ["env", "region"],           // Override @dims
  vars: { ROOT: "/app" },            // Custom ${VAR:*} values
  schema: (cfg) => validate(cfg),    // Validation hook
  warnOnSkippedIncludes: false,

  // Dotenv integration
  dotenv: true,                      // Load .env and .env.local
  // dotenv: ".env.production",      // Or specify a path
  // dotenv: [".env", ".env.local"], // Or multiple paths

  // Security sandbox
  sandbox: {
    basePath: "./config",
    allowedEnvVars: ["NODE_ENV", "API_KEY"],
    allowedVars: ["ROOT"],
    maxIncludeDepth: 50,
    maxFileSize: 1048576,
  },

  // Secrets
  secrets: {
    providers: [new AwsSecretsProvider()],
    allowedSecrets: ["myapp/*"],
    cache: { enabled: true, ttl: 300000 },
    onNotFound: "warn",
  },
});

Returns: ColonyConfig object with:

// Direct property access
config.database.host;

// Methods
config.get("database.host");           // Dot-notation access
config.keys();                         // List all leaf keys
config.explain("database.host");       // Debug info
config.toJSON();                       // Plain object
config.diff(otherConfig);              // Compare configs

// Internal (non-enumerable)
config._warnings;                      // Array of warnings
config._trace;                         // Map of trace info

validateColony(entry)

Validate syntax without resolving.

import { validateColony } from "@ant.sh/colony";

const { valid, files, errors } = await validateColony("./config/app.colony");

if (!valid) {
  for (const { file, error } of errors) {
    console.error(`${file}: ${error}`);
  }
}

diffColony(options)

Compare configs with different contexts.

import { diffColony } from "@ant.sh/colony";

const { cfg1, cfg2, diff } = await diffColony({
  entry: "./config/app.colony",
  ctx1: { env: "dev" },
  ctx2: { env: "prod" },
});

console.log("Added in prod:", diff.added);
console.log("Removed in prod:", diff.removed);
console.log("Changed:", diff.changed);

lintColony(options)

Find potential issues in config files.

import { lintColony } from "@ant.sh/colony";

const { issues } = await lintColony({
  entry: "./config/app.colony",
  dims: ["env"],
});

for (const issue of issues) {
  console.log(`[${issue.severity}] ${issue.type}: ${issue.message}`);
}

Issue types:

  • parse_error - Syntax error
  • shadowed_rule - Rule overwritten by later rule
  • overridden_wildcard - Wildcard always overridden
  • empty_include - Include pattern matches no files

CLI Reference

colony print

Print resolved configuration.

colony print --entry ./config/app.colony --ctx "env=prod,region=us"
colony print --entry ./config/app.colony --ctx "env=prod" --format json
colony print --entry ./config/app.colony --ctx "env=prod" --query "database"

Options:

  • --entry, -e - Entry file path (required)
  • --ctx, -c - Context as key=value pairs
  • --format, -f - Output format: json (default) or yaml
  • --query, -q - Print only matching key path

colony diff

Compare two contexts.

colony diff --entry ./config/app.colony --ctx1 "env=dev" --ctx2 "env=prod"

colony validate

Validate config syntax.

colony validate --entry ./config/app.colony

Exit codes: 0 = Valid, 1 = Invalid

colony lint

Check for potential issues.

colony lint --entry ./config/app.colony

colony includes

List all included files.

colony includes --entry ./config/app.colony

colony env

Show environment variables used.

colony env --entry ./config/app.colony

Secrets Management

Colony integrates with secret managers to keep credentials out of config files.

Config Syntax

*.db.password = "${AWS:myapp/database#password}";
*.api.key = "${VAULT:secret/data/myapp#api_key}";
*.token = "${OPENBAO:secret/data/app#token}";

Pattern: ${PROVIDER:path#field}

  • PROVIDER - Provider prefix (AWS, VAULT, OPENBAO, or custom)
  • path - Secret path/name in the provider
  • field - Optional JSON field to extract

AWS Secrets Manager

import { loadColony, AwsSecretsProvider } from "@ant.sh/colony";

const config = await loadColony({
  entry: "./config/app.colony",
  secrets: {
    providers: [
      new AwsSecretsProvider({ region: "us-east-1" }),
    ],
  },
});

Requirements:

  • npm install @aws-sdk/client-secrets-manager
  • AWS credentials configured (env vars, IAM role, etc.)

HashiCorp Vault

import { loadColony, VaultProvider } from "@ant.sh/colony";

const config = await loadColony({
  entry: "./config/app.colony",
  secrets: {
    providers: [
      new VaultProvider({
        addr: "https://vault.example.com",
        token: process.env.VAULT_TOKEN,
      }),
    ],
  },
});

Environment variables:

  • VAULT_ADDR - Vault server address
  • VAULT_TOKEN - Authentication token
  • VAULT_NAMESPACE - Optional namespace

OpenBao

import { loadColony, OpenBaoProvider } from "@ant.sh/colony";

const config = await loadColony({
  entry: "./config/app.colony",
  secrets: {
    providers: [new OpenBaoProvider()],
  },
});

Custom Providers

import { registerSecretProvider } from "@ant.sh/colony";

registerSecretProvider({
  prefix: "CUSTOM",
  fetch: async (key) => {
    return await mySecretStore.get(key);
  },
});

// Now ${CUSTOM:path} works in config files

Security Options

secrets: {
  providers: [...],
  allowedSecrets: ["myapp/*", "shared/db-*"],  // Whitelist
  cache: {
    enabled: true,
    ttl: 300000,      // 5 minutes
    maxSize: 100,
  },
  onNotFound: "warn",  // "empty" | "warn" | "error"
}

Security

Sandbox Options

When loading untrusted config files:

const config = await loadColony({
  entry: untrustedPath,
  sandbox: {
    basePath: "./config",              // Restrict @include
    allowedEnvVars: ["NODE_ENV"],      // Whitelist env vars
    allowedVars: ["ROOT"],             // Whitelist custom vars
    maxIncludeDepth: 10,               // Prevent infinite loops
    maxFileSize: 1048576,              // 1MB max
  },
});

Path Traversal Protection

When basePath is set, Colony blocks includes that resolve outside:

@include "../../../etc/passwd";  # Blocked!
@include "/etc/passwd";          # Blocked!

Warnings

Access _warnings for security-related warnings:

const config = await loadColony({ ... });

for (const warning of config._warnings) {
  console.log(`[${warning.type}] ${warning.message}`);
}

Warning types:

  • blocked_env_var - Blocked by allowedEnvVars
  • blocked_var - Blocked by allowedVars
  • blocked_secret - Blocked by allowedSecrets
  • unknown_var - Variable not found
  • secret_not_found - Secret not found in provider

TypeScript

Colony includes full TypeScript definitions.

import {
  loadColony,
  LoadColonyOptions,
  ColonyConfig,
  SecretProvider,
  Warning,
} from "@ant.sh/colony";

// Options are fully typed
const options: LoadColonyOptions = {
  entry: "./config/app.colony",
  ctx: { env: "prod" },
};

// Config object is typed
const config: ColonyConfig = await loadColony(options);

// Custom provider with type safety
const provider: SecretProvider = {
  prefix: "CUSTOM",
  fetch: async (key: string): Promise<string> => {
    return "value";
  },
};

Key Types

interface LoadColonyOptions {
  entry: string;
  dims?: string[];
  ctx?: Record<string, string>;
  vars?: Record<string, string>;
  schema?: (cfg: ColonyConfig) => ColonyConfig | Promise<ColonyConfig>;
  sandbox?: SandboxOptions;
  warnOnSkippedIncludes?: boolean;
  secrets?: SecretsOptions;
}

interface ColonyConfig {
  get(path: string): unknown;
  explain(path: string): TraceInfo | null;
  toJSON(): Record<string, unknown>;
  keys(): string[];
  diff(other: ColonyConfig | Record<string, unknown>): DiffResult;
  readonly _trace: Map<string, TraceInfo>;
  readonly _warnings: Warning[];
  [key: string]: unknown;
}

Examples

Basic App Config

config/app.colony:

@dims env;

# App settings
*.app.name = "MyApp";
*.app.version = "1.0.0";

# Server
*.server.port = 3000;
*.server.host = "localhost";
prod.server.host = "0.0.0.0";

# Database
*.database.host = "localhost";
*.database.port = 5432;
prod.database.host = "prod-db.internal";

# Logging
*.log.level = "debug";
prod.log.level = "info";

app.js:

import { loadColony } from "@ant.sh/colony";

const config = await loadColony({
  entry: "./config/app.colony",
  ctx: { env: process.env.NODE_ENV || "dev" },
});

console.log(`Starting ${config.app.name} v${config.app.version}`);
console.log(`Server: ${config.server.host}:${config.server.port}`);

With Secrets

config/app.colony:

@dims env;

*.database.host = "localhost";
*.database.user = "app";
*.database.password = "${ENV:DB_PASSWORD}";

prod.database.host = "prod-db.internal";
prod.database.password = "${AWS:myapp/prod/db#password}";

app.js:

import { loadColony, AwsSecretsProvider } from "@ant.sh/colony";

const config = await loadColony({
  entry: "./config/app.colony",
  ctx: { env: process.env.NODE_ENV || "dev" },
  secrets: {
    providers: [new AwsSecretsProvider({ region: "us-east-1" })],
  },
});

With Schema Validation (Zod)

import { loadColony } from "@ant.sh/colony";
import { z } from "zod";

const configSchema = z.object({
  database: z.object({
    host: z.string(),
    port: z.number(),
    name: z.string(),
  }),
  server: z.object({
    port: z.number().min(1).max(65535),
  }),
});

const config = await loadColony({
  entry: "./config/app.colony",
  ctx: { env: "prod" },
  schema: (cfg) => configSchema.parse(cfg),
});

Modular Config with Includes

config/app.colony:

@dims env;
@include "./base.colony";
@include "./database.colony";
@include "./envs/${ctx.env}.colony";

config/base.colony:

*.app.name = "MyApp";
*.app.version = "1.0.0";

config/database.colony:

*.database.port = 5432;
*.database.pool.min = 2;
*.database.pool.max = 10;

config/envs/prod.colony:

prod.database.host = "prod-db.internal";
prod.database.pool.min = 10;
prod.database.pool.max = 50;

Framework Integrations

Express.js

import express from 'express';
import { loadColony } from '@ant.sh/colony';

const config = await loadColony({
  entry: './config/app.colony',
  ctx: { env: process.env.NODE_ENV || 'dev' },
  dotenv: true,
});

const app = express();

app.get('/health', (req, res) => {
  res.json({ status: 'ok', env: process.env.NODE_ENV });
});

app.listen(config.server.port, () => {
  console.log(`Server running on port ${config.server.port}`);
});

Fastify

import Fastify from 'fastify';
import { loadColony } from '@ant.sh/colony';

const config = await loadColony({
  entry: './config/app.colony',
  ctx: { env: process.env.NODE_ENV || 'dev' },
  dotenv: true,
});

const fastify = Fastify({
  logger: config.log.enabled,
});

fastify.get('/health', async () => {
  return { status: 'ok' };
});

await fastify.listen({
  port: config.server.port,
  host: config.server.host,
});

Next.js

// lib/config.js
import { loadColony } from '@ant.sh/colony';

let configPromise = null;

export async function getConfig() {
  if (!configPromise) {
    configPromise = loadColony({
      entry: './config/app.colony',
      ctx: { env: process.env.NODE_ENV || 'development' },
      dotenv: ['.env', '.env.local'],
    });
  }
  return configPromise;
}

// app/api/config/route.js
import { getConfig } from '@/lib/config';

export async function GET() {
  const config = await getConfig();
  return Response.json({
    appName: config.app.name,
    features: config.features,
  });
}

Docker Compose

# docker-compose.yml
services:
  app:
    build: .
    environment:
      NODE_ENV: production
    command: >
      sh -c "
        colony print -e ./config/app.colony -c 'env=prod' -f json > /app/config.json &&
        node server.js
      "

Kubernetes ConfigMap

# Generate config JSON
colony print -e ./config/app.colony -c 'env=prod,region=us-east-1' -f json > config.json

# Create ConfigMap
kubectl create configmap myapp-config --from-file=config.json

Troubleshooting

Debug Which Rule Set a Value

const config = await loadColony({ ... });
const trace = config.explain("database.host");
console.log(trace);
// {
//   op: "=",
//   scope: ["prod"],
//   specificity: 1,
//   filePath: "/path/to/config/app.colony",
//   line: 15,
//   source: "/path/to/config/app.colony:15:1"
// }

List All Config Keys

const keys = config.keys();
// ["app.name", "app.version", "database.host", ...]

Compare Environments

colony diff --entry ./config/app.colony --ctx1 "env=dev" --ctx2 "env=prod"

Check for Issues

colony lint --entry ./config/app.colony