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.
- One API surface:
createConfigClientworks 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.
Install the package (or link it locally while developing):
npm install comfyconfigRequires 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();
}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.
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"
}
});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 testThis 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.
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 buildOutputs 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.
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
});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-idEnvironment 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.jsNeed 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.
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-apiAlready 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 (nonode_modulesin 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 ./azureUseful 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 containingfunction.jsonand any other assets (defaults to the shipped template).--provider– choosefile(default) orazureto control the runtime provider;--use-app-configis a shorthand for--provider azure.--config-path– path to the feature config file bundled for file deployments (defaults toconfig/features.json).--app-config-endpoint– URL of an Azure App Configuration instance when using theazureprovider. If omitted, the deploy script will create a new store viainfra/appconfig.bicepand use its endpoint.--app-config-name– override the generated App Configuration name when provisioning one.--app-config-sku– set the SKU (FreeorStandard) for newly created stores (defaults to the template’sStandard).--app-config-reader-principal/--app-config-reader-type– optionally grantApp Configuration Data Readerto an identity during provisioning.--label– optional App Configuration label to publish during Azure sync flows.--sync-azure– set totrueto 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).
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.
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 \
--pruneUseful flags:
--path– override the JSON source file (defaults toconfig/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.
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 --pruneSwap in your preferred CI runner or authentication flow—the script only needs a valid Azure credential in the environment.
- Keep
config/features.jsonin the repo and review changes like code. - Use
createConfigClientwithkind: "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.