Offline definitions

Commit a pinned copy of your redaction rules, exclusions and firewall policy so a server that starts while ApiTraffic is unreachable still applies them.

This page as Markdown

Your SDK fetches redaction rules, exclusions and firewall policy from ApiTraffic when your application starts. If it cannot reach us on boot - our outage, a network partition, your egress rules - it falls back through a ladder of local sources instead of coming up with nothing.

The last tier of that ladder is a file you commit to your repository. This page is about keeping it fresh, which is the part that decides whether it helps you. For everything else the SDK does while ApiTraffic is down - telemetry, startup, memory - see If ApiTraffic is unreachable.

The whole picture

There are two loops. One runs on every server, on every boot: find configuration, apply it, and keep trying to get back to live. The other is how the file at the bottom of that ladder stays current.

Loop 1 - every boot

mermaid
flowchart TB
    boot([Your app starts]) --> live{"Reach<br/>ApiTraffic?"}
    live -- yes --> apply["Apply live config,<br/>write the disk cache"]
    live -- no --> cache{"Disk cache?"}
    cache -- yes --> fb["Apply it.<br/>Capture and firewall<br/>work normally."]
    cache -- no --> mirror{"CDN mirror?"}
    mirror -- yes --> fb
    mirror -- no --> pinned{"Pinned file<br/>in the image?"}
    pinned -- yes --> fb
    pinned -- no --> blind["No config.<br/>Capture off, logged as an error.<br/>Firewall per offlineWafMode."]
    fb -. "retries forever, jittered" .-> live
    blind -. retries forever .-> live

Live always wins. A fallback is never stickier than the real thing: the first successful live load replaces whatever tier the server was on, without a restart.

Loop 2 - keeping the file current

mermaid
flowchart TB
    edit([You edit a rule in ApiTraffic]) --> hash["The bucket's config hash changes"]
    hash --> servers["Connected servers reload<br/>within milliseconds"]
    hash --> pr["ApiTraffic opens or updates<br/>a pull request"]
    hash --> badge["Servers page flags images<br/>carrying a stale file"]
    pr --> merge([You review and merge])
    cli(["Or run: definitions pull"]) --> commit
    merge --> commit[".apitraffic/definitions/sdk.json<br/>is current again"]
    ci{{"CI: definitions check"}} -. "fails a build that would ship<br/>stale fallback rules" .-> cli
    commit --> image([Your next image ships the current rules])

The resolution ladder

Every load attempt tries these in order:

Tier Source Works on a brand-new container?
Live api.apitraffic.io - this is the normal path
Cache node_modules/.cache/apitraffic/ no - the cache dies with the image
Mirror ApiTraffic’s CDN mirror, a separate failure domain yes, once the server has reached us once
Pinned .apitraffic/definitions/sdk.json in your repo yes, always

Info: Live always wins. The SDK keeps retrying while it runs on a fallback, and the moment ApiTraffic answers it switches to live config - no restart, no redeploy. A fallback is never stickier than the real thing.

The pinned file is the only tier that covers the case people actually worry about: an autoscaling group launching fresh containers from a fresh image while we are down.

The disk cache is written on every successful live load and needs nothing from you. On a read-only filesystem - a deliberate, correct choice in a hardened container - the write simply fails and the tier is skipped; it is a debug log, never an error. Point it somewhere writable with API_TRAFFIC_CACHE_DIR if you want it back.

What an outage actually looks like

A server that boots while ApiTraffic is unreachable, and what happens when we come back:

mermaid
sequenceDiagram
    autonumber
    participant App as Your app
    participant SDK as ApiTraffic SDK
    participant File as Pinned file
    participant AT as ApiTraffic

    App->>SDK: start up
    SDK->>AT: GET /sdk
    AT--xSDK: unreachable
    SDK->>File: read .apitraffic/definitions/sdk.json
    File-->>SDK: redactions, exclusions, firewall policy
    Note over SDK: Capture resumes.<br/>Firewall enforces your policy.<br/>Reports configSource "pinned".
    App->>App: serves traffic normally

    loop every ~60s, jittered
        SDK->>AT: GET /sdk
    end

    AT-->>SDK: 200 - live config
    Note over SDK: Switches to live.<br/>No restart. No redeploy.<br/>Rewrites the disk cache.

Note: The jitter is not decoration. Without it, a five-hundred-server fleet that lost configuration together would reconnect as a single burst the moment we returned - which is how a recovering service gets knocked back down.

Create the file

Install nothing - the CLI runs straight from npx, and authenticates with the same ingestion key your SDK already uses.

bash
export API_TRAFFIC_TOKEN="your-ingestion-key"
export API_TRAFFIC_BUCKET="k3m9p2x"

npx @apitraffic/cli definitions pull

That writes .apitraffic/definitions/sdk.json. Commit it. Every flag and every failure mode is in the CLI reference.

You can also download the same file in ApiTraffic, if you would rather not run anything:

Open API Tokens

In your account settings, under Integration, select API Tokens.

Open the definitions dialog

In the section for the environment your servers use, choose Definitions. The Offline Definitions dialog opens.

Download the file

Select the Bucket, then choose Download definitions. Save the file at the path the dialog shows, .apitraffic/definitions/sdk.json, in the repository you deploy.

Download a bucket’s definitions from the API Tokens page.

Offline Definitions dialog with a bucket selected, a Download definitions button, the .apitraffic/definitions/sdk.json path and a definitions pull command

Or call the endpoint directly.

Warning: This file contains your firewall policy and your redaction rules. A redaction rule can name internal fields; a firewall policy describes exactly what you block and how. Both are useful to an attacker. Commit it to a private repository - never a public one.

Keep it fresh

A committed file goes stale the moment somebody edits a rule in the dashboard, and the person editing rules is usually not the person deploying. A stale file that predates a newly added redaction rule will not redact that field on the fallback path.

That is the one real trade-off this feature makes, and the check below is how you stay ahead of it.

bash
npx @apitraffic/cli definitions check
Exit code Meaning
0 the committed file matches your live config
1 it has drifted, was edited by hand, is missing, or is pinned to another bucket
2 the check could not run - bad token, network failure, or a bad response

Drift and “could not run” are deliberately different codes, so an ApiTraffic blip does not have to fail your build. See Choosing how strict to be.

Note: A drift failure is not an outage. Servers already running hold live config and are completely unaffected. The check is a warning about the next cold start that cannot reach us - which is exactly why it is worth knowing about while everything is fine.

CI recipes

GitHub Actions

yaml
name: ApiTraffic definitions

on:
  pull_request:
  push:
    branches: [main]

jobs:
  definitions:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Check pinned definitions
        # --ignore-unreachable: drift still fails the build; an ApiTraffic outage does not.
        run: npx @apitraffic/cli definitions check --ignore-unreachable
        env:
          API_TRAFFIC_TOKEN: ${{ secrets.API_TRAFFIC_TOKEN }}
          API_TRAFFIC_BUCKET: ${{ vars.API_TRAFFIC_BUCKET }}

Run it on pull requests. That puts the staleness in front of the person doing the deploy, at the moment of the deploy, which is the only moment anyone can act on it.

Note: Keep --ignore-unreachable in your pipeline. Without it, definitions check exits 2 whenever it cannot reach ApiTraffic, and that fails your build - making our availability a dependency of your deploys. With it, drift and a rejected token still fail; only “could not reach ApiTraffic” passes. Drop it only if the pinned file is a compliance control and you would rather stop a deploy than ship one you could not verify. See Choosing how strict to be.

GitLab CI

yaml
apitraffic:definitions:
  image: node:20
  script:
    - npx @apitraffic/cli definitions check --ignore-unreachable
  variables:
    API_TRAFFIC_BUCKET: k3m9p2x
  # API_TRAFFIC_TOKEN comes from a masked CI/CD variable

CircleCI

yaml
jobs:
  apitraffic-definitions:
    docker:
      - image: cimg/node:20.11
    steps:
      - checkout
      - run:
          name: Check ApiTraffic definitions
          command: npx @apitraffic/cli definitions check --ignore-unreachable

As a pre-commit hook

bash
npx @apitraffic/cli definitions check --warn-only --quiet || true

--warn-only prints the drift and still exits 0 - useful locally, where blocking a commit on someone else’s dashboard edit is the wrong trade.

Choosing how strict to be

The default fails the build on drift and on a failed check. Two flags relax that, and which you want depends on what the file is protecting.

Strict (default)

bash
npx @apitraffic/cli definitions check

Fails on drift and when the check cannot run. Right when the pinned file is a compliance control: you would rather stop a deploy than ship one you could not verify.

Don’t fail on our outage

bash
npx @apitraffic/cli definitions check --ignore-unreachable

Drift still fails the build; an unreachable ApiTraffic does not. Most teams want this - our availability should not be a dependency of your deploys.

Note that a rejected token still fails: that is a real misconfiguration, not a blip.

Advisory only

bash
npx @apitraffic/cli definitions check --warn-only

Always exits 0, prints the drift. Good for adopting the check without blocking anyone, as long as somebody is actually reading the log.

Automating the refresh

The check tells you the file is stale. Refreshing it is one command:

bash
npx @apitraffic/cli definitions pull
git add .apitraffic/definitions/sdk.json
git commit -m "chore: refresh ApiTraffic definitions"

Let ApiTraffic open the pull request

If you have connected a repository under Git integrations, ApiTraffic can do this for you. When a bucket’s configuration changes, it commits the new definitions to a branch in the repository linked to that bucket and opens a pull request - which turns config drift from an ops chore into a code review, with a diff showing exactly which redaction rule changed and who changed it.

How it behaves:

  • One branch per bucket, apitraffic/definitions/{bucketSid}. A bucket edited five times in a morning updates one pull request five times - it does not open five.
  • The file is byte-identical to what the CLI writes, so running definitions pull locally and receiving these pull requests never produces a formatting-only diff.
  • Changes are debounced. Adding four redaction rules in a row produces one pull request.
  • Closing it is an opt-out. It is not reopened for the same configuration.
  • Nothing is merged for you. ApiTraffic opens the pull request; merging is yours.
mermaid
sequenceDiagram
    autonumber
    participant You as You (dashboard)
    participant AT as ApiTraffic
    participant Repo as Your repository
    participant Team as Your team

    You->>AT: add a redaction rule
    Note over AT: Config hash changes.<br/>Connected servers pick it up<br/>within milliseconds.
    AT->>AT: wait ~30s, coalescing further edits
    AT->>Repo: commit to apitraffic/definitions/{bucketSid}
    AT->>Repo: open (or update) the pull request
    Repo-->>Team: review - the diff shows which rule changed
    Team->>Repo: merge
    Note over Repo: The next image built from main<br/>ships the current rules.

Note: The pull request is never urgent. Your running servers already have the configuration in it - they fetched it live. Merging keeps the fallback honest for the next cold start.

Several buckets in one repository

Each service pins its own bucket:

bash
API_TRAFFIC_BUCKET=p4n8w2k npx @apitraffic/cli definitions pull --bucket-qualified
API_TRAFFIC_BUCKET=s7q3m9d npx @apitraffic/cli definitions pull --bucket-qualified

That writes .apitraffic/definitions/sdk.p4n8w2k.json and sdk.s7q3m9d.json. Each SDK prefers the file matching its own bucket and falls back to a shared sdk.json.

A file whose bucketSid does not match the SDK’s configured bucket is refused, not best-guessed. Silently applying another service’s redaction rules would be a data protection incident dressed up as a convenience.

What the dashboard shows

Every server reports which tier it is running on, and the hash of the pinned file in its image - whichever tier it actually used. The Servers page turns that into a per-row badge:

Badge Meaning
(none) running on live config, no pinned file
Pinned: current the file in this image matches your live config
Pinned: out of date this server is fine now; its next cold start would apply stale rules
Running on fallback this server could not reach ApiTraffic and is on a local tier now
No config it reached nothing and has no file - capture is off

The file itself is never uploaded. Only the hash travels, so we can tell you whether it matches without ever holding a copy of your rules.

Where the file lives

Resolution order, checked by the SDK and by the CLI identically:

  1. API_TRAFFIC_DEFINITIONS_PATH, if set - an override, and then the only place looked at
  2. .apitraffic/definitions/sdk.{bucketSid}.json
  3. .apitraffic/definitions/sdk.json

Both walk up from the working directory, at most six levels, so starting your process from a subdirectory still finds the file at the repository root. The SDK stops at the first directory that contains .git or a workspace file (pnpm-workspace.yaml, lerna.json, nx.json, turbo.json, rush.json), or failing that a package.json, so it never reads a file from a shared parent directory. If you set projectRoot (or API_TRAFFIC_PROJECT_ROOT), that directory is checked first. The CLI stops at the first directory containing .git, because a file above your checkout is not one you can commit.

Warning: The SDK refuses a definitions file that someone else could have written. On Linux and macOS, a file that is group- or world-writable, or owned by a user other than the one running your app (root is allowed), is ignored with the console message ApiTraffic: not reading definitions from …. The file decides what is redacted, so it must not be editable by other users. Keep the default 0644 permissions the CLI writes, and if your image build changes ownership, make the file owned by root or by the app’s user.

Every CLI flag and failure mode is documented in the CLI reference.

What the firewall does

On the cache, mirror and pinned tiers the firewall has a real policy, so it behaves exactly as it does on live configuration - your rules are enforced, in the mode you chose.

The only question is the bottom of the ladder: a server that reached nothing at all, so there is no policy to enforce. offlineWafMode decides what happens to traffic in that state, and only in that state:

Setting Behavior when no policy has ever loaded
inherit (default) Allow traffic
open Allow traffic
closed Refuse every request with 503 Service Unavailable

@apitraffic/node

javascript
app.use(apitraffic.express({ waf: { offlineWafMode: 'closed' } }));

@apitraffic/waf

javascript
const waf = require('@apitraffic/waf');

waf.configure({ offlineWafMode: 'closed' });

Environment

bash
API_TRAFFIC_OFFLINE_WAF_MODE=closed

The environment variable overrides the code setting, so you can flip this during an incident without a redeploy - which is exactly when you would want to.

Warning: The default lets traffic through. An ApiTraffic outage must not take your site down. A failMode of closed on your policy does not cover this case - that setting is about an evaluation failing with a policy loaded, and here there is no policy at all. If you need the strict answer, set offlineWafMode explicitly.

Warning: closed is the one setting with which an ApiTraffic outage can take your application down. A server that boots while we are unreachable, with no cached, mirrored or pinned policy, refuses every request with 503 until a policy loads. If you set it, commit the pinned file so that a fresh container always has a policy to enforce. Removing or disabling the firewall policy for a bucket does not trigger it: servers that have loaded settings with no policy allow traffic. Details: If ApiTraffic is unreachable.

The refusal is 503, not 403: nothing is wrong with the request, we simply cannot vouch for it, and that distinction matters to whoever reads the logs afterwards.

Either way the SDK logs at error on this tier - it is the one state where it is knowingly running blind, and it says so.

Security and privacy

What exactly is in the file?

Your redaction rules, exclusions, firewall policy, APM capture switches and host telemetry settings - the same configuration your SDK downloads on every boot.

It contains no credentials. The realtime token in the live payload is minted per client with a short lifetime, and is excluded from the snapshot by construction, so there is never a secret at rest in your repository.

Why does it need to be a private repository?

A firewall policy describes precisely what you block and how, and a redaction rule can name internal fields, which reveals schema. Both are genuinely useful to an attacker.

This is the trade-off of the pinned tier: it works during a total outage because it is in your repository, and that is also what makes it readable by everyone with repository access.

Does the file get uploaded back to ApiTraffic?

No. Servers report the hash of the file in their image - about 80 bytes - never its contents. That is enough to tell you a file is stale without us ever holding a copy of your rules.

What if the file is stale and misses a new redaction rule?

On the fallback path, that field is not redacted. This is the one real correctness trade-off the feature makes, and it is why the drift check and the automatic pull request exist rather than being optional extras.

It is still strictly better than the alternatives: without a fallback, an affected server drops all telemetry; and we will not ship unredacted data to make up for a missing rule.

Can a file from another bucket be applied by mistake?

No. An SDK refuses a snapshot whose bucketSid is not its own, logs it, and falls through to the next tier. Silently applying another service’s redaction rules would be a data-protection incident dressed up as a convenience.

Does the pinned file expire?

No - by design. Availability wins, and a two-year-old redaction rule still redacts.

Past 30 days the SDK logs a warning and definitions check says so even when the file matches, because a file that old usually means nobody owns refreshing it. If you want a hard limit, set API_TRAFFIC_FALLBACK_MAX_AGE_DAYS and the SDK will skip a file older than that, falling through to the next tier.

Common questions

Do I have to commit a file for any of this to work?

No. The disk cache and the CDN mirror need nothing from you and cover most real outages - our application being down while object storage is not, or a container restarting on a host that already has the cache.

The pinned file covers what those structurally cannot: a brand-new container from a fresh image, during an outage, with no prior contact. If that is a scenario you care about - autoscaling during an incident, or egress locked to a single host - commit the file.

Does a server on a fallback tier stay there?

Only until we are reachable. Retries continue for the lifetime of the process - backing off to about a minute and holding there, jittered - and the first success switches it to live configuration with no restart.

Does this cover telemetry delivery too?

No. This is about configuration, not ingest. Telemetry has its own retry queue for blips, but a long outage of the ingest host means data is not delivered. What this feature guarantees is that your server starts, serves traffic, applies your firewall policy, and redacts the fields you told it to.

Will a stale file block my deploy?

Only if you ask it to. definitions check returns a distinct exit code for drift, and you choose whether your pipeline treats it as a failure, a warning, or nothing at all - see Choosing how strict to be.

My server shows an amber badge. Am I unprotected right now?

Almost certainly not. Pinned: out of date means the server is running live configuration and is completely fine - the warning is about its next cold start during an outage.

Running on fallback is the badge that means “this server cannot reach us right now”. It is still applying your rules, from a local tier, and will heal itself when we return.

  • CLI: Every command, flag and exit code.

  • If ApiTraffic is unreachable: What the SDK does to your application during an outage.

  • Servers: See which servers carry a current or stale definitions file.

  • Git integrations: Connect a repository so ApiTraffic can open the pull requests.