Skip to main content
darkship docs
Open console

Start · Build with AI

Integrate Darkship with a coding agent

This page is written for an AI coding agent (and for the developer who supervises it). Give the agent this URL and one sentence of context, and it has what it needs to add feature flags to your project: how the system works, which key goes where, tested OpenFeature providers, and the rules to follow.

Updated Oct 28, 202617 min read Edit this page on GitHub (opens in a new tab)

How to use this page#

Paste the prompt below into your agent (Claude Code, Cursor, Copilot, Codex or similar), replacing the bracketed parts. If the agent cannot browse, give it /llms-full.txt, which contains every docs page as plain text, or the OpenAPI document at /openapi.json.

Outputprompt
Integrate Darkship feature flags into this project. Read and follow
https://darkship.tech/docs/ai/integrate/ exactly. Use OpenFeature with the provider from
that page (there is no Darkship SDK package to install yet).

Context:
- The flag to add: [flag key, type, what it controls, safe default]
- Where it is evaluated: [server route / component / job]
- Environment variables are already set: DARKSHIP_BASE_URL and DARKSHIP_CLIENT_KEY
  (or tell me where to put them).
Do not create or change flags in production without asking me.

What the system is#

  • Darkship is a feature flag service. A flag has a key, a type and per-environment targeting (an on/off switch, target lists, rules, a default). Evaluating a flag for a context (a user key plus attributes) returns a value, the variation that produced it and a reason.
  • Flag types: boolean, string, int, float, json, date. The type is fixed when the flag is created. Flag keys match ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ (use kebab-case, for example new-checkout-flow).
  • Hierarchy: organisation, project, environment (for example dev, staging, production). A key belongs to exactly one environment, so the same code behaves differently per environment just by changing the key.
  • Evaluation order: the environment switch, then target lists, then rules top to bottom (first match wins), then the default. Percentage rollouts hash the context key, so always send a stable targetingKey.
  • Evaluation never throws. When anything is wrong (flag missing, wrong type, service unreachable) the caller gets its own default value and an error code. Always choose a default that is safe if the flag cannot be read.

Hosts#

Production hosts.
PurposeURL
Evaluation (SDK data plane)https://sdk.darkship.tech
Management APIhttps://api.darkship.tech/v1
Consolehttps://app.darkship.tech

Keys#

Which credential goes where. Create keys in the console under Environments, SDK keys. A key's full value is shown once.
CredentialPrefixUse it forWhere it may live
Client SDK keyds_cli_POST /v1/sdk/evaluate (remote evaluation). This is what the providers below use.Environment variable or build config. Designed to be public; it only reveals resolved values.
Server SDK keyds_srv_Downloading the whole ruleset for local evaluation (needs a native SDK, not available yet).Secret store only. Never in a browser, mobile app or repository.
API tokends_pat_Management API: create flags, toggle them, manage targeting.Secret store only. Scoped; prefer the narrowest scopes.

The integration path today#

No SDK package is published yet

Do not run npm install @darkship/sdk or pip install darkship and do not look for them on a registry. Until the SDKs are published, integrate through OpenFeature (the vendor-neutral standard) with the small provider on this page, which calls the remote evaluation endpoint. Do not re-implement rule evaluation yourself; that logic belongs to the SDKs and to the service.

  • Your application code only talks to the standard OpenFeature client (getBooleanValue and friends). The provider is the only Darkship-specific file.
  • When the native SDKs ship, only the provider registration changes; the flag calls in your code stay exactly the same.
  • Limitation: remote evaluation only serves flags that are enabled for client-side use (the flag's client-side setting in the console, client_side: true in the API). Enabling it means anyone holding the client key can read that flag's resolved value for any context they supply, so do not use it for secrets. A flag that is not client-side returns FLAG_NOT_FOUND.
  • Each remote evaluation call counts as a request against the plan quota. The providers cache the flags for a context for 10 seconds, which is usually enough; raise cacheTtlMs for hot paths.

Steps for the agent#

  1. Inspect the project. Find the language, framework, package manager, where configuration lives, how tests are written, and whether OpenFeature is already used (search for openfeature). Reuse existing conventions. If OpenFeature is already present, add only the provider.
  2. Install the OpenFeature SDK for the runtime: @openfeature/server-sdk (Node), @openfeature/web-sdk (browser) or openfeature-sdk (Python). Other languages: use that language's OpenFeature SDK and implement a provider from the evaluation contract.
  3. Add configuration. Two variables, never hard-coded: DARKSHIP_BASE_URL=https://sdk.darkship.tech and DARKSHIP_CLIENT_KEY=ds_cli_... (one key per environment). Add them to the project's example env file with an empty key, never a real value. Do not commit keys.
  4. Add the provider file from the next section, adapted to the project's style (imports, lint rules, logging). Keep it in one module, for example src/lib/darkship-provider.
  5. Register it once at startup and wait for it to be ready, then use OpenFeature.getClient() anywhere. Do not create a provider per request.
  6. Evaluate with a safe default and a stable context. Pass targetingKey (the user, organisation or anonymous-visitor id) and any attributes your rules use (plan, country, orgId...). Keep the old behaviour as the default so that nothing changes if the flag is missing.
  7. Wrap the feature in a single, small conditional. One flag controls one decision; avoid scattering the same flag over many files, and avoid nested flags.
  8. Test without the network. Use OpenFeature's in-memory provider in unit tests (InMemoryProvider in the JavaScript SDKs, InMemoryProvider in Python) so tests never call Darkship. Cover both flag states and the default path.
  9. Verify with the checklist at the end of this page and tell the human which flags must exist, with which type, default and client-side setting.

Providers#

These providers were tested against the production evaluation service. They resolve boolean, string, number and object flags, map reasons and errors to the OpenFeature standard, apply a request timeout, never cache a failed request, and bound the cache. date flags arrive as ISO-8601 strings (use string evaluation); int and float flags use number evaluation; json flags use object evaluation.

Node.js (server)#

Shellterminal
$ npm install @openfeature/server-sdk
TypeScriptsrc/lib/darkship-provider.ts
import {
  ErrorCode,
  StandardResolutionReasons,
  type EvaluationContext,
  type JsonValue,
  type Provider,
  type ResolutionDetails,
} from '@openfeature/server-sdk';

type Wire = {
  value: JsonValue;
  variation_key: string | null;
  reason: 'OFF' | 'TARGET_MATCH' | 'RULE_MATCH' | 'FALLTHROUGH' | 'ERROR';
  error_code?: string;
  bucket?: number;
  flag_type?: 'boolean' | 'string' | 'int' | 'float' | 'json' | 'date';
};

export interface DarkshipProviderOptions {
  /** Evaluation host, e.g. https://sdk.darkship.tech (no trailing path). */
  baseUrl: string;
  /** Client key (ds_cli_...). Remote evaluation only accepts client keys. */
  clientKey: string;
  timeoutMs?: number;
  /** How long the flags resolved for one context are reused. */
  cacheTtlMs?: number;
}

const MAX_CACHED_CONTEXTS = 1000;

/** OpenFeature context -> wire context: `targetingKey` becomes `key`, Dates become ISO strings. */
function toWireContext(ctx: EvaluationContext): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const [k, v] of Object.entries(ctx)) {
    if (k === 'targetingKey' || v === undefined) continue;
    out[k] = v instanceof Date ? v.toISOString() : v;
  }
  if (typeof ctx.targetingKey === 'string' && ctx.targetingKey !== '') out.key = ctx.targetingKey;
  return out;
}

function mapReason(w: Wire): string {
  switch (w.reason) {
    case 'OFF': return StandardResolutionReasons.STATIC;
    case 'TARGET_MATCH':
    case 'RULE_MATCH': return w.bucket === undefined ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.SPLIT;
    case 'FALLTHROUGH': return w.bucket === undefined ? StandardResolutionReasons.DEFAULT : StandardResolutionReasons.SPLIT;
    default: return StandardResolutionReasons.ERROR;
  }
}

export class DarkshipProvider implements Provider {
  readonly metadata = { name: 'darkship-remote' } as const;
  readonly runsOn = 'server' as const;

  private readonly cache = new Map<string, { at: number; flags: Promise<Record<string, Wire>> }>();

  constructor(private readonly opts: DarkshipProviderOptions) {}

  resolveBooleanEvaluation(key: string, def: boolean, ctx: EvaluationContext) {
    return this.resolve(key, def, ctx, (w) => typeof w.value === 'boolean');
  }
  resolveStringEvaluation(key: string, def: string, ctx: EvaluationContext) {
    return this.resolve(key, def, ctx, (w) => typeof w.value === 'string');
  }
  resolveNumberEvaluation(key: string, def: number, ctx: EvaluationContext) {
    return this.resolve(key, def, ctx, (w) => typeof w.value === 'number');
  }
  resolveObjectEvaluation<T extends JsonValue>(key: string, def: T, ctx: EvaluationContext) {
    return this.resolve(key, def, ctx, (w) => typeof w.value === 'object' && w.value !== null);
  }

  private async resolve<T extends JsonValue>(
    flagKey: string,
    def: T,
    ctx: EvaluationContext,
    matchesType: (w: Wire) => boolean,
  ): Promise<ResolutionDetails<T>> {
    const fail = (errorCode: ErrorCode, errorMessage: string): ResolutionDetails<T> => ({
      value: def,
      reason: StandardResolutionReasons.ERROR,
      errorCode,
      errorMessage,
    });
    let flags: Record<string, Wire>;
    try {
      flags = await this.flagsFor(ctx);
    } catch (err) {
      return fail(ErrorCode.GENERAL, `darkship: evaluation request failed (${(err as Error).message})`);
    }
    const w = Object.hasOwn(flags, flagKey) ? flags[flagKey] : undefined;
    if (w === undefined || w.error_code === 'FLAG_NOT_FOUND') {
      return fail(ErrorCode.FLAG_NOT_FOUND, `darkship: flag "${flagKey}" not found, or not enabled for client-side use`);
    }
    if (w.reason === 'ERROR') return fail(w.error_code === 'INVALID_CONTEXT' ? ErrorCode.INVALID_CONTEXT : ErrorCode.GENERAL, `darkship: ${w.error_code ?? 'error'}`);
    if (!matchesType(w)) return fail(ErrorCode.TYPE_MISMATCH, `darkship: flag "${flagKey}" is a ${w.flag_type ?? 'different'} flag`);
    return {
      value: w.value as T,
      variant: w.variation_key ?? undefined,
      reason: mapReason(w),
      flagMetadata: { ...(w.flag_type ? { flagType: w.flag_type } : {}), ...(w.bucket !== undefined ? { bucket: w.bucket } : {}) },
    };
  }

  /** All client-visible flags for one context, cached briefly and shared by concurrent callers. */
  private flagsFor(ctx: EvaluationContext): Promise<Record<string, Wire>> {
    const wireCtx = toWireContext(ctx);
    const cacheKey = JSON.stringify(Object.entries(wireCtx).sort(([a], [b]) => a.localeCompare(b)));
    const hit = this.cache.get(cacheKey);
    if (hit && Date.now() - hit.at < (this.opts.cacheTtlMs ?? 10_000)) return hit.flags;
    const flags = this.fetchFlags(wireCtx);
    this.cache.set(cacheKey, { at: Date.now(), flags });
    flags.catch(() => this.cache.delete(cacheKey)); // never cache a failure
    if (this.cache.size > MAX_CACHED_CONTEXTS) this.cache.delete(this.cache.keys().next().value as string);
    return flags;
  }

  private async fetchFlags(context: Record<string, unknown>): Promise<Record<string, Wire>> {
    const res = await fetch(`${this.opts.baseUrl.replace(/\/+$/, '')}/v1/sdk/evaluate`, {
      method: 'POST',
      headers: { authorization: `Bearer ${this.opts.clientKey}`, 'content-type': 'application/json' },
      body: JSON.stringify({ context }),
      signal: AbortSignal.timeout(this.opts.timeoutMs ?? 2000),
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return ((await res.json()) as { flags: Record<string, Wire> }).flags;
  }
}
TypeScriptstartup and use
import { OpenFeature } from '@openfeature/server-sdk';
import { DarkshipProvider } from './lib/darkship-provider';

await OpenFeature.setProviderAndWait(
  new DarkshipProvider({
    baseUrl: process.env.DARKSHIP_BASE_URL!,
    clientKey: process.env.DARKSHIP_CLIENT_KEY!,
  }),
);

const flags = OpenFeature.getClient();

// Per request: stable targetingKey + the attributes your rules use.
const useNewCheckout = await flags.getBooleanValue(
  'new-checkout-flow',
  false, // safe default: the old behaviour
  { targetingKey: user.id, plan: user.plan, orgId: org.id },
);

The evaluation contract#

For any other language, implement a provider from this contract. It is the same endpoint the providers above call (full schema in the API explorer).

HTTPrequest
POST https://sdk.darkship.tech/v1/sdk/evaluate
Authorization: Bearer ds_cli_...
Content-Type: application/json

{ "context": { "key": "user-123", "plan": "pro", "country": "BR" } }
JSONresponse (200)
{
  "version": "snapshot-version",
  "flags": {
    "new-checkout-flow": { "value": true, "variation_key": "on", "reason": "FALLTHROUGH", "flag_type": "boolean" },
    "max-seats": { "value": 25, "variation_key": "pro", "reason": "RULE_MATCH", "flag_type": "int" },
    "retired-flag": { "value": null, "variation_key": null, "reason": "ERROR", "error_code": "FLAG_NOT_FOUND" }
  }
}
  • context is required and must be a JSON object. Put the stable user or organisation id in key; every other attribute is free-form (strings, numbers, booleans, ISO-8601 dates, arrays of strings). Do not send secrets or more personal data than your rules need.
  • To evaluate only some flags add "flag_keys": ["a", "b"] (up to 1000). POST /v1/sdk/evaluate/{flagKey} evaluates one flag and returns that entry plus flag_key and version.
  • Responses are never cacheable on the HTTP level (no-store); cache in your client. Honour Retry-After on 429.
Wire reason to OpenFeature reason.
Wire reasonOpenFeature reason
OFFSTATIC
TARGET_MATCH, RULE_MATCHTARGETING_MATCH, or SPLIT when bucket is present
FALLTHROUGHDEFAULT, or SPLIT when bucket is present
ERROR (with error_code)ERROR
Errors and what the provider returns. In every error case the application receives its own default value.
SituationOpenFeature error code
Flag absent, archived, not client-side, or from another environment (FLAG_NOT_FOUND)FLAG_NOT_FOUND
Value type differs from the requested typeTYPE_MISMATCH
Context rejected (INVALID_CONTEXT)INVALID_CONTEXT
Timeout, network failure, HTTP 401/403/5xxGENERAL

Managing flags from the agent (optional)#

Flags can be created in the console or through the management API with an ds_pat_ API token (created under the console's API tokens; give it only the flag scopes it needs). Look up the exact schemas in /openapi.json. The calls an agent usually needs:

Management endpoints on https://api.darkship.tech. projectId and envId come from the list calls.
GoalCall
List projects and environmentsGET /v1/projects, GET /v1/projects/{projectId}/environments
List and read flagsGET /v1/projects/{projectId}/flags, GET /v1/projects/{projectId}/flags/{flagKey}
Create a flagPOST /v1/projects/{projectId}/flags with { "key", "name", "type", "client_side": true }
Switch a flag on or off in one environmentPOST /v1/projects/{projectId}/flags/{flagKey}/environments/{envId}/toggle with { "enabled": true }
Read or change targetingGET, PATCH or PUT /v1/projects/{projectId}/flags/{flagKey}/environments/{envId}
Preview an evaluationPOST /v1/projects/{projectId}/flags/{flagKey}/environments/{envId}/evaluate-preview
Create a client SDK keyPOST /v1/projects/{projectId}/environments/{envId}/sdk-keys with { "role": "client", "name": "..." }
Shellterminal
$ curl -X POST https://api.darkship.tech/v1/projects/$PROJECT_ID/flags \
    -H "Authorization: Bearer $DARKSHIP_API_TOKEN" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d '{ "key": "new-checkout-flow", "name": "New checkout flow", "type": "boolean", "client_side": true }'

Mutations accept an Idempotency-Key header and flags return an ETag to send back in If-Match when updating. See the REST API reference for conventions, pagination and errors.

Rules for the agent#

Do#

  • Use a safe default at every call site: the pre-flag behaviour, so a missing flag or an outage changes nothing.
  • Pass a stable targetingKey. For anonymous visitors, generate and persist an anonymous id; do not use a value that changes on every request, or percentage rollouts will flicker.
  • Register the provider once, and reuse the client. Keep flag evaluation off tight loops; evaluate once per request or render and pass the result down.
  • Name flags by capability in kebab-case (new-checkout-flow, export-csv), match the type to the use (boolean for on/off, string for variants, int for limits), and remove the flag and the dead branch once a rollout is complete.
  • Mock the provider in tests and cover both branches.
  • Ask the human before creating, changing or toggling flags in a production environment.

Do not#

  • Do not put a ds_srv_ or ds_pat_ value in browser code, mobile apps, repositories, logs or error messages. Do not print any key.
  • Do not put personal data, secrets or card numbers in flag keys, names, descriptions or rule values, and do not send more context attributes than the rules use.
  • Do not re-implement targeting or percentage bucketing, and do not call the evaluation endpoint on every render without a cache.
  • Do not use a flag for authorisation. Flags decide what to show or enable; permissions are enforced on your server.
  • Do not catch and hide evaluation errors in a way that loses the default; the OpenFeature client already returns the default.

Verify#

  1. The flag exists in the target environment, has the intended type, and has client-side enabled.
  2. The key is a ds_cli_ key for the same environment, and the variables are set.
  3. Check the raw response from a shell (replace the key; the value of $KEY is a client key):
Shellterminal
$ curl -s https://sdk.darkship.tech/v1/sdk/evaluate \
    -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
    -d '{ "context": { "key": "user-1" } }'
  1. Evaluate once through OpenFeature and print the details (getBooleanDetails): check value, variant, reason and, if something is wrong, errorCode.
  2. Run the project's tests and linter; make sure no key appears in the diff.
Troubleshooting.
SymptomLikely cause
HTTP 401 or 403Key missing, revoked, from another environment, or the wrong type (a ds_srv_ or ds_pat_ key on the evaluation endpoint).
FLAG_NOT_FOUNDTypo in the key, flag archived, flag is not enabled for client-side use, or the key belongs to a different environment.
TYPE_MISMATCHThe call type differs from the flag type (for example a string call on a boolean flag).
Always the defaultService unreachable or timing out (check DARKSHIP_BASE_URL and the network), or the environment has no published ruleset yet.
HTTP 429Rate limited. Raise the cache TTL and honour Retry-After.
Changes take a few seconds to appearExpected: the provider caches for 10 seconds per context and does not stream updates.

Related#

Was this helpful?