Operations

Troubleshooting

Diagnose API, authentication, database, bootstrap, and local evaluation problems in FlagForge

Troubleshooting

FlagForge failures usually become easier to isolate by checking the API first, then the credential type, environment name, and SDK configuration. Use this order before changing flag targeting.

Start with the service

Run the unauthenticated health check against the host and port where the API is listening:

bash
curl -i http://localhost:4000/health

A healthy response has status 200 and a body like:

json
{"status":"ok","uptime":12.34}

The endpoint checks both the running API process and database connectivity. A 503 response with "status":"degraded" means the process is up but the database check failed. Inspect DATABASE_URL, confirm that the configured database is available, and restart the API after correcting its environment.

Note:

/health does not require an API key. Use /docs for the interactive Swagger UI and /openapi.json for the live API contract once the service responds.

1

Check the service, credentials, and resource names

2
  1. Confirm that http://localhost:4000/health returns status: ok.

  2. Send the request with an explicit Authorization: Bearer <api-key> header and the correct key type.

  3. Verify that the project, flag, and environment names in the request are the ones configured in FlagForge.

Authentication failures

Every protected request uses a Bearer token:

bash
curl -i http://localhost:4000/v1/projects \
  -H "Authorization: Bearer $FLAGFORGE_SERVER_KEY"

Check the response and credential prefix against this table:

SymptomLikely causeCheck
401 with Missing or malformed Authorization headerThe header is absent, misspelled, or not in Bearer formUse exactly Authorization: Bearer <api-key>; avoid quotes becoming part of the token.
401 with Invalid API keyThe token is incorrect or not present in this API databaseCompare the value with the server or client key issued by this FlagForge instance.
403 on a management routeA client key was used for project managementUse a srv_ server key. Client keys are for evaluation.
403 on a different projectA project-scoped server key is outside its scopeUse the key belonging to the requested project or a global server key.
400 saying evaluation requires a project-scoped keyA global server key was used on an evaluation routeUse a project-scoped cli_ client key, or a project-scoped server key.

Client keys are project-scoped and should be used by the SDK. Server keys are for management operations. Do not put a server key in application code that may be exposed to users.

Database and startup problems

The API validates its environment configuration at startup. The relevant settings are PORT, HOST, LOG_LEVEL, DATABASE_URL, and ADMIN_SERVER_KEY. If you do not set them, the defaults include port 4000, host 0.0.0.0, log level info, and SQLite URL file:./dev.db.

For a local SQLite setup, run the database setup command before starting the service:

bash
pnpm --filter @flagforge/api db:setup
pnpm --filter @flagforge/api dev

For PostgreSQL, set DATABASE_URL to the connection URL supported by your Prisma configuration before running setup. If startup fails or /health returns 503, check for these common issues:

  • The URL has a typo, uses an unreachable host, or contains invalid credentials.

  • The database schema has not been created for the current URL.

  • The running process did not receive the environment variables you changed.

  • A relative SQLite URL points to a different working directory than expected.

Note:

Changing DATABASE_URL can make an otherwise valid key, project, or flag appear to be missing because the API is now connected to a different database.

SDK bootstrap failures

FlagForgeClient.init() calls GET /v1/config?environment=<environment> and must complete before local evaluations use bootstrapped definitions. A failed bootstrap rejects init() with an error in this form:

text
FlagForge bootstrap failed: 401 Unauthorized

Use this direct check to separate SDK configuration from API behavior:

bash
curl -i "http://localhost:4000/v1/config?environment=production" \
  -H "Authorization: Bearer $FLAGFORGE_CLIENT_KEY"

Then verify the following:

  • baseUrl is the API origin, such as http://localhost:4000, without an extra /v1 path.

  • clientKey is a valid cli_ key for the project containing the flags.

  • environment exactly matches the environment key; names are not interchangeable with display names.

  • The API and SDK are using the same FlagForge instance and database.

  • The URL is reachable from the application process, not merely from your workstation.

The SDK removes one trailing slash from baseUrl, but it does not repair an incorrect host, path, key, or environment. In Node.js runtimes without a global fetch, provide the SDK's fetch option.

Unexpected evaluation results

Use detailedVariation() while diagnosing instead of looking only at a typed value or fallback:

typescript
const result = client.detailedVariation("new-checkout", {
  key: user.id,
  attributes: { plan: user.plan, country: user.country },
});

console.log(result);

The result includes variationKey, value, and a reason. The normal reasons identify the path taken:

ReasonMeaningWhat to inspect
OFFThe flag is disabledThe flag's off variation and enabled state.
RULE_MATCHAn ordered targeting rule matchedRule conditions, segment membership, and the returned variation key.
FALLTHROUGHNo targeting rule matchedThe environment's defaultVariationKey.
ERROREvaluation could not resolve a valid variationVariation keys referenced by the flag, rules, rollouts, and off/default configuration.

Targeting is evaluated per environment. Confirm that the SDK environment and the environment used when managing the flag are identical. For a rollout, use a stable context.key; changing it changes the deterministic bucket. If a rollout uses an attribute, ensure that attribute exists and has the expected value in context.attributes.

If a flag is unknown or evaluation returns an error, variation() returns its supplied default. Typed helpers also protect their return type: getString, getNumber, and getJson fall back when the resolved value is not the expected type. This can make a configuration problem look like a valid default, so inspect detailedVariation() during diagnosis.

Polling and change listeners

Polling is disabled when pollIntervalMs is 0 or omitted. A positive interval starts background refresh after the initial init() succeeds:

typescript
const client = new FlagForgeClient({
  baseUrl: "http://localhost:4000",
  clientKey: process.env.FLAGFORGE_CLIENT_KEY!,
  environment: "production",
  pollIntervalMs: 30_000,
});

client.onChange(() => {
  console.log("Flag definitions refreshed");
});

await client.init();

If changes are not appearing, check that:

  • pollIntervalMs is a positive number in milliseconds.

  • The process remains able to reach the API and the client key remains valid.

  • The listener is registered on the same client instance that was initialized.

  • You are not expecting polling before init() has completed.

A background refresh that fails is ignored by the polling timer; call refresh() yourself when you need to observe and handle the rejection. Call close() when the client is no longer needed to stop polling and release listeners.

Self-hosting the API
Configure the service and database.
SDK client guide
Review bootstrap, typed getters, and lifecycle methods.
How evaluation works
Understand targeting rules, segments, and rollouts.