Skip to content

instafluff/ComfyConfig

Repository files navigation

ComfyConfig

The comfiest way to ship, test, and debug feature flags without the usual overhead. ComfyConfig gives every project a lightweight config client that works the same in the browser, on the server, and in local scripts.

Highlights

  • One API surface: createConfigClient works in Node, browsers, tests, and CLI tools.
  • Provider plug-ins for Azure App Configuration, HTTP gateways, local files, or in-memory fixtures.
  • Safe-by-default evaluation: user allowlists, validation, caching, and stale-on-error resilience.
  • Built-in debugging helpers so you always know which snapshot is loaded.

Quick Start

Install the package (or link it locally while developing):

npm install comfyconfig

Requires Node.js 22 or newer for the CLI, build pipeline, and Azure Functions deploy script.

Create a client wherever you need feature flags:

import { createConfigClient } from "comfyconfig";

const config = await createConfigClient({
	userId: currentUser.email,
	provider: {
		kind: "http",
		endpoint: "/api/config"
	},
	authToken: msalAccessToken,
	ttlMs: 30_000
});

if (await config.isEnabled("newDashboard")) {
	renderNewDashboard();
}

Sample Feature Definition

Track flags in source control using config/features.json (included in this repo as a template):

{
	"version": "2025-11-09T00:00:00Z",
	"features": [
		{ "name": "newDashboard", "enabled": true, "description": "Opt-in to the redesigned dashboard layout." },
		{ "name": "betaApi", "enabled": false, "description": "Experimental API endpoints for the mobile team.", "allowlist": ["jane@example.com", "mobile-team@example.com"] },
		{ "name": "supportChat", "enabled": true, "description": "Surface concierge chat to select customers.", "allowlist": ["vip@example.com", "support@example.com"] }
	]
}

Sync this file to Azure App Configuration (e.g., via CI) or serve it straight from your gateway in development.

Providers At A Glance

import { createConfigClient } from "comfyconfig";

// Backend with Azure App Configuration + Managed Identity
const serverConfig = await createConfigClient({
	userId: req.user?.email,
	provider: {
		kind: "azure",
		endpoint: process.env.APP_CONFIG_ENDPOINT!
	}
});

// Browser via a secure gateway endpoint
const browserConfig = await createConfigClient({
	userId: currentUser.email,
	authToken: msalAccessToken,
	provider: {
		kind: "http",
		endpoint: "/api/config"
	}
});

// CLI or local dev straight from disk
const localConfig = await createConfigClient({
	provider: {
		kind: "file",
		path: "./config/features.json"
	}
});

Testing & Debugging

Spin up an isolated, in-memory client for unit tests or storybook fixtures:

import { createTestConfigClient } from "comfyconfig";

const testClient = await createTestConfigClient({
	version: "test",
	features: [
		{ name: "newDashboard", enabled: true }
	]
}, { userId: "jane@example.com" });

expect(await testClient.isEnabled("newDashboard")).toBe(true);

Inspect the current snapshot, TTL, and any recent provider errors without breaking the flow:

const snapshot = testClient.debug();
console.log(snapshot);

Run the automated test suite:

npm test

This runs the production build and executes the co-located *.test.ts files that live beside each module. Add new cases next to the code they verify so context stays close.

Bundles & Size Profiling

Run the build to emit both the ESM module and a browser-friendly IIFE bundle, complete with sourcemaps and esbuild metadata for profiling:

npm run build

Outputs land in build/:

  • build/index.js – ship this when targeting modern bundlers or Node runtimes (ESM).

  • build/comfyconfig.global.js – load directly in the browser via <script>:

     <script src="/vendor/comfyconfig.global.js"></script>
     <script>
     	const client = await ComfyConfig.createBrowserClient({
     		userId: window.currentUser.email,
     		authToken: window.accessToken,
     		endpoint: "/api/config"
     	});
     </script>
  • build/module.meta.json / build/browser.meta.json – inspect size breakdowns when you need to chase down regressions.

Feature Service

Serve the same feature config through a lightweight Express app, perfect for local development, Azure Functions, or containerised gateways. Import the backend helpers from the dedicated server entry so the client bundle stays slim:

import { createConfigServer } from "comfyconfig/server";

const app = await createConfigServer({
	provider: {
		kind: "file",
		path: "./config/features.json"
	},
	resolveUserId: ( request ) => request.headers[ "x-ms-client-principal-name" ] ?? request.headers[ "x-user-id" ],
	middlewares: [
		(req, _res, next) => {
			if( !req.headers.authorization ) {
				return next( new Error( "Missing auth" ) );
			}
			next();
		}
	]
});

app.listen( 4000, () => console.log( "Feature config ready" ) );

The handler only returns flags that are active for the resolved user. By default ComfyConfig looks for x-user-id, x-ms-client-principal-name, or x-client-principal-name and responds with 401 if none are present. Override resolveUserId when you need to derive the user from a JWT, cookie, or session store.

Need a one-liner instead? Call the convenience helper:

import { startConfigServer } from "comfyconfig/server";

await startConfigServer({
	provider: { kind: "file", path: "./config/features.json" },
	port: 4000
});

CLI for local runs

Skip the boilerplate and launch the gateway directly from the terminal. The CLI ships with the package so npx works out of the box:

npx comfyconfig serve --provider file --file ./config/features.json --port 4000 --user-header x-user-id

Environment variables mirror the flags (COMFYCONFIG_PROVIDER, COMFYCONFIG_ENDPOINT, COMFYCONFIG_FILE, etc.), making it easy to wrap this in Docker or a Procfile.

Need custom authentication or payload trimming? Supply a hook module that exports Express middleware, a user resolver, and/or a beforeSend transformer:

// server-hooks.js
export const middlewares = [
	(req, res, next) => {
		if( !req.headers.authorization?.startsWith( "Bearer " ) ) {
			return res.status( 401 ).json( { error: "Unauthorized" } );
		}
		next();
	},
];

export const resolveUserId = ( request ) =>
	request.headers[ "x-ms-client-principal-name" ]
		?? request.headers[ "x-user-id" ];

export const beforeSend = ( config, request ) => ({
	...config,
	features: config.features.filter( ( feature ) => feature.enabled || request.headers[ "x-debug" ] === "1" ),
});

Run it with:

npx comfyconfig serve --provider azure --endpoint "$APP_CONFIG_ENDPOINT" --config ./server-hooks.js

Need to plug the same logic into Azure Functions? Create a handler that slots straight into @azure/functions:

import { app } from "@azure/functions";
import { createAzureFunctionHandler } from "comfyconfig/server";

const handler = createAzureFunctionHandler({
	provider: {
		kind: "azure",
		endpoint: process.env.APP_CONFIG_ENDPOINT!
	},
	resolveUserId: ( request ) => request.headers[ "x-ms-client-principal-name" ]
});

app.http("config", {
	methods: ["GET"],
	handler
});

Use the optional beforeSend hook to trim or transform the config per request, and pass any Express middleware (CORS, auth, logging) via the middlewares array for maximum flexibility.

Looking for a starting point? Copy the scaffold in templates/azure-function/config/ which includes function.json, an example handler, and notes on wiring Managed Identity.

Azure Functions Deployment

Ship the sample gateway straight to Azure Functions with a single command. Make sure you have the Azure CLI installed, run az login, and then:

npm install comfyconfig
npx comfyconfig deploy --resource-group my-comfyconfig-rg --region eastus \
	--function-app comfyconfig-api

Already working inside this repository? npm run deploy continues to wrap the same command for convenience, but end users can run comfyconfig deploy from any project after installing the package.

By default the sample bundles config/features.json and serves it with the file provider, so you can deploy without touching Azure App Configuration. When you're ready to switch, pass --provider azure --app-config-endpoint <url> (and optionally --label / --sync-azure). Want the script to stand up App Configuration for you? Add --use-app-config (or --provider azure) and it will deploy infra/appconfig.bicep, create a globally unique store, and wire the resulting endpoint into the Function App automatically. Supply --app-config-name if you want to control the name, plus --app-config-reader-principal / --app-config-reader-type to grant access during deployment.

The deploy script will:

  • build the library (unless you pass --skip-build),
  • package the Azure Function from templates/azure-function/config/,
  • bundle your handler, comfyconfig, and its dependencies into a single entry file (no node_modules in the zip),
  • stand up the Function App + storage account via infra/functionapp.bicep, and
  • push the zip via az functionapp deployment source config-zip.

By default the handler uses the template in templates/azure-function/config/index.ts. Customize it (or point at your own file) to plug in middleware, alternate user resolution, or additional hooks:

npx comfyconfig deploy --handler ./azure/index.ts --template ./azure

Useful flags:

  • --resource-group / --region – where to provision the Function App (defaults: <package-slug>-eastus-rg, eastus).
  • --function-app – Explicit Function App name (defaults to <package-slug>-<function>-<region> when omitted).
  • --storage-account – custom backing storage account (generated deterministically from the package + region if omitted; existing app settings are reused when redeploying).
  • --subscription – Azure subscription ID to override the CLI default.
  • --handler – path to the TypeScript/JavaScript entry file for the function.
  • --template – directory containing function.json and any other assets (defaults to the shipped template).
  • --provider – choose file (default) or azure to control the runtime provider; --use-app-config is a shorthand for --provider azure.
  • --config-path – path to the feature config file bundled for file deployments (defaults to config/features.json).
  • --app-config-endpoint – URL of an Azure App Configuration instance when using the azure provider. If omitted, the deploy script will create a new store via infra/appconfig.bicep and use its endpoint.
  • --app-config-name – override the generated App Configuration name when provisioning one.
  • --app-config-sku – set the SKU (Free or Standard) for newly created stores (defaults to the template’s Standard).
  • --app-config-reader-principal / --app-config-reader-type – optionally grant App Configuration Data Reader to an identity during provisioning.
  • --label – optional App Configuration label to publish during Azure sync flows.
  • --sync-azure – set to true to opt into config sync steps when using App Configuration.
  • --yes – skip the interactive confirmation prompt.
  • --keep-temp – leave the generated artifact folder on disk for inspection.

Behind the scenes the script leverages infra/functionapp.bicep for the Function App + storage account and infra/appconfig.bicep when you opt into App Configuration, so you can inspect or customise those templates as needed.

After a successful run you can call the Function endpoint at https://<function-app>.azurewebsites.net/api/<function-name> (default config).

Ship Flags Through Azure Pipelines

1. Provision App Configuration (Bicep)

Deploy infra/appconfig.bicep to your Azure subscription. It creates an App Configuration instance with a system-assigned managed identity and can optionally grant App Configuration Data Reader to a caller:

az deployment group create \
	--resource-group my-rg \
	--template-file infra/appconfig.bicep \
	--parameters \
		name=my-comfyconfig \
		readerPrincipalId=<OBJECT_ID_OF_CALLER>

Outputs include the endpoint and the managed identity principal ID you can authorize elsewhere.

2. Push features.json during CI/CD

The repo ships a helper script that syncs config/features.json into Azure App Configuration using DefaultAzureCredential (great for GitHub Actions OIDC, Azure DevOps, or local az login). It keeps feature flags and allowlists aligned, writes the config version, and can clean up orphaned keys.

# requires `npm run build` so the validator is compiled
npm run build
npm run sync:azure -- \
	--endpoint https://my-comfyconfig.azconfig.io \
	--label production \
	--prune

Useful flags:

  • --path – override the JSON source file (defaults to config/features.json).
  • --label – isolate environments (production, staging, etc.).
  • --dry-run – print operations without making changes.
  • --prune – delete feature flag or allowlist keys that no longer exist in the config.

3. Example GitHub Actions job

jobs:
	sync-flags:
		runs-on: ubuntu-latest
		permissions:
			id-token: write
			contents: read
		steps:
			- uses: actions/checkout@v4
			- uses: actions/setup-node@v4
				with:
					node-version: 22
			- run: npm ci
			- run: npm run build
			- name: Login to Azure
				uses: azure/login@v2
				with:
					client-id: ${{ secrets.AZURE_CLIENT_ID }}
					tenant-id: ${{ secrets.AZURE_TENANT_ID }}
					subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
			- run: npm run sync:azure -- --endpoint https://my-comfyconfig.azconfig.io --label production --prune

Swap in your preferred CI runner or authentication flow—the script only needs a valid Azure credential in the environment.

Production Checklist

  • Keep config/features.json in the repo and review changes like code.
  • Use createConfigClient with kind: "azure" on the server for direct MSI access.
  • Frontends should call a lightweight gateway that authenticates the user, trims the response, and returns a validated feature config.
  • Monitor client.debug() in health endpoints or status pages to detect stale snapshots early.

About

The comfiest way to manage and deploy feature flags for apps

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Sponsor this project

  •  

Packages

No packages published