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?" →
envdimension (dev, staging, prod) - "Which region am I deployed to?" →
regiondimension (us, eu, asia) - "Which customer is this for?" →
tenantdimension (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 = 3000wins (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 |
|---|---|---|
= | Set | Set value, overwrites existing |
:= | Set if missing | Set only if key doesn't exist |
|= | Merge | Deep merge objects, overwrite primitives |
+= | Append | Append to array (creates array if needed) |
-= | Remove | Remove 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 errorshadowed_rule- Rule overwritten by later ruleoverridden_wildcard- Wildcard always overriddenempty_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) oryaml--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 providerfield- 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 addressVAULT_TOKEN- Authentication tokenVAULT_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 allowedEnvVarsblocked_var- Blocked by allowedVarsblocked_secret- Blocked by allowedSecretsunknown_var- Variable not foundsecret_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