New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

@apitraffic/strapi

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@apitraffic/strapi

Strapi plugin for ApiTraffic: request capture and the ApiTraffic firewall, with APM through @apitraffic/apm, installed with one dependency.

latest
Source
npmnpm
Version
0.3.1
Version published
Maintainers
1
Created
Source




Website   •   Docs   •   Strapi Docs   •   Strapi Marketplace   •   X   •   Discord

npm license

@apitraffic/strapi is the ApiTraffic plugin for Strapi 5. It captures every request your Strapi app serves - method, URL, matched route, headers, request and response bodies, status and timing - and the outbound http, https and fetch calls your Strapi code makes, and sends them to your ApiTraffic bucket. It also runs the ApiTraffic firewall and starts APM with one preload flag.

Strapi 5 middlewares are Koa middlewares, so the plugin is @apitraffic/node's Koa integration with Strapi's configuration plumbing around it: capture by @apitraffic/koa, then the firewall by @apitraffic/waf, exactly as @apitraffic/node mounts them in a plain Koa app. One implementation, the same guarantees. The plugin adds no routes, content types, controllers or admin screens to your app; it only observes, and with a firewall policy switched on protects, the requests your app already serves.

Contents

Requirements

  • Strapi 5 (@strapi/strapi ^5.0.0, the plugin's peer dependency). Strapi 4 is not supported.
  • @apitraffic/strapi 0.3 or later. This README describes 0.3: 0.2 has no firewall and no APM, and earlier versions have no sdk service and none of the options below.
  • Node.js 20 or later.
  • An ingest token and a bucket ID from your ApiTraffic account. The setup page in ApiTraffic creates the token for you if you are an owner or admin; see API tokens.
  • Outbound HTTPS from your servers to ingest.apitraffic.io and api.apitraffic.io. Live configuration changes (redaction rules, exclusions, firewall policy) are pushed over a realtime connection to Ably, so also allow its realtime hosts; if that connection is blocked, live updates wait for a restart and nothing else is affected.

An active ApiTraffic account is required. Start a free trial if you do not have one yet.

Installation

npm install @apitraffic/strapi
pnpm add @apitraffic/strapi
yarn add @apitraffic/strapi

@apitraffic/node (and with it @apitraffic/koa, @apitraffic/waf and @apitraffic/apm) is installed with it as a dependency; you do not need to add it yourself.

Quick start

1. Add the middleware. Add plugin::apitraffic.apitraffic to the array in config/middlewares.js (or config/middlewares.ts). Leave the middlewares Strapi ships with in place and add it after them:

// config/middlewares.js
module.exports = [
  'strapi::logger',
  'strapi::errors',
  'strapi::security',
  'strapi::cors',
  'strapi::poweredBy',
  'strapi::query',
  'strapi::body',
  'strapi::session',
  'strapi::favicon',
  'strapi::public',
  'plugin::apitraffic.apitraffic',
];

Installing the plugin is not enough on its own: without this entry, nothing is captured.

2. Set your credentials. Add your token and bucket ID to Strapi's .env file, or set them wherever you set your other environment variables. Strapi loads .env itself.

# .env
API_TRAFFIC_TOKEN=your-ingest-token
API_TRAFFIC_BUCKET=your-bucket-id

3. Restart Strapi and make a request. Restart Strapi so it reads the variables, then call any endpoint, for example GET /api/articles. The setup page in ApiTraffic shows that the SDK is connected and waiting for a request, then Traffic is flowing when the request arrives.

Strapi enables installed plugins on its own, so config/plugins.js is only needed if you want to set options in code (see Configuration).

Where to put the middleware

Put plugin::apitraffic.apitraffic after Strapi's built-in middlewares, as in the example above:

  • After strapi::errors, so a request whose controller throws is reported with the status Strapi's error handling sends.
  • After strapi::body, so the request body is reported as Strapi parsed it, and the firewall's body rules see it.
  • A middleware listed before ApiTraffic that answers a request without calling next() ends that request before ApiTraffic sees it. With the default list, that means static files served by strapi::public, /favicon.ico served by strapi::favicon and CORS preflights answered by strapi::cors are not captured - usually what you want.

Listing the middleware twice (or mounting @apitraffic/node yourself as well) is harmless: each request is reported, and checked by the firewall, once.

Configuration

Where settings come from

The plugin reads its options from three places:

  • The plugin's configuration in config/plugins.js (or .ts), under apitraffic.config.
  • API_TRAFFIC_* environment variables, read when the SDK starts.
  • Settings delivered from your ApiTraffic account - redaction rules, exclusions and the rest - loaded on startup and updated in real time, without a restart.

An option set in the plugin configuration wins over its environment variable, which wins over the built-in default. The plugin's default configuration is:

token: env('API_TRAFFIC_TOKEN'),
bucket: env('API_TRAFFIC_BUCKET'),

so with no config/plugins.js at all, the token and bucket come from the environment, and every other API_TRAFFIC_* variable from the SDK configuration reference applies as well - for example API_TRAFFIC_DEBUG=true or API_TRAFFIC_INTERCEPT_OUTBOUND=false.

A few settings combine differently (for example captureStatementParams and captureRedisKeys are on if either your configuration or the bucket's APM Capture settings turn them on, and a blank interceptOutbound turns outbound capture off). See Precedence.

Setting options in code

Any option from the SDK configuration reference can go in the plugin's configuration. Everything under config is passed straight to @apitraffic/node's Koa integration:

// config/plugins.js
module.exports = ({ env }) => ({
  apitraffic: {
    config: {
      token: env('API_TRAFFIC_TOKEN'),
      bucket: env('API_TRAFFIC_BUCKET'),
      debug: env.bool('API_TRAFFIC_DEBUG', false),
      requestSampleRate: 0.5,
    },
  },
});
// config/plugins.ts
export default ({ env }) => ({
  apitraffic: {
    config: {
      token: env('API_TRAFFIC_TOKEN'),
      bucket: env('API_TRAFFIC_BUCKET'),
      maxBodyBytes: 262144,
    },
  },
});

strict is not parsed from strings: pass true or false (Strapi's env.bool() does this for you). waf and debug also accept the words API_TRAFFIC_WAF_ENABLED does (false, 0, off, true, 1, on, any case), so a value read with env() rather than env.bool() still turns them off. API_TRAFFIC_DEBUG turns debug logging on only when it is exactly true (any case); 1 or yes do not. (The package's own warnings about errors it swallowed print whenever the variable is set to any non-empty value.) The other switches and limits accept strings too (true/false, yes/no, on/off, 1/0; whole numbers for byte caps; 0–1 for rates).

The token and bucket must reach the plugin configuration - from the default above, or from your own config/plugins.js. Options can also be given on the middleware entry in config/middlewares.js ({ name: 'plugin::apitraffic.apitraffic', config: { ... } }), but the plugin configuration wins for any key set in both, and a token or bucket given only there does not enable capture.

Options

The commonly used options. Every option is optional in code; token and bucket are required from code or the environment.

OptionEnvironment variableTypeDefaultEffect
tokenAPI_TRAFFIC_TOKENstring-Required. Your ingest token.
bucketAPI_TRAFFIC_BUCKETstring-Required. The ID of the bucket captured traffic is sent to.
debugAPI_TRAFFIC_DEBUGbooleanfalseLog SDK activity to the console. Useful while installing; leave it off in production.
wafAPI_TRAFFIC_WAF_ENABLEDboolean or object-false refuses the firewall locally, whatever your account says; an object passes settings through to @apitraffic/waf. Normally left unset: the firewall is switched on from your account. See Firewall.
strict-booleanfalseMakes the SDK's setup() throw on misconfiguration instead of logging. With this plugin it does not stop Strapi from booting; see the note under Failure behaviour.
interceptOutboundAPI_TRAFFIC_INTERCEPT_OUTBOUNDswitchtrueCapture outbound calls made with http, https and fetch. A blank value turns it off.
captureFetchAPI_TRAFFIC_CAPTURE_FETCHswitchtrueCapture outbound global fetch calls (requires interceptOutbound).
redactSensitiveKeysAPI_TRAFFIC_REDACT_SENSITIVE_KEYSswitchtrueMask credential-shaped body keys and query parameters (password, token, secret, api_key and similar). Credential headers are always masked.
maxBodyBytesAPI_TRAFFIC_MAX_BODY_BYTESbytes524288 (512 KB)Largest request or response body sent. A larger body is cut to the cap and ends with …[truncated by ApiTraffic: N bytes total]. Applied after redaction.
maxRedactBodyBytesAPI_TRAFFIC_MAX_REDACT_BODY_BYTESbytes1048576 (1 MB)Largest body the SDK redacts; a larger one becomes a placeholder before any redaction work.
maxFetchBodyLengthAPI_TRAFFIC_MAX_FETCH_BODY_LENGTHbytes524288 (512 KB)Largest outbound request or response body captured.
requestSampleRateAPI_TRAFFIC_REQUEST_SAMPLE_RATErate1Fraction of requests sent. 5xx responses are always sent, so 0 sends server errors only.
ingestBatchAPI_TRAFFIC_INGEST_BATCHswitchtrueSend several payloads per request to ApiTraffic when your account and the ingest host support it.
ingestGzipAPI_TRAFFIC_INGEST_GZIPswitchtrueCompress those batches when your account asks for it.
apiHostAPI_TRAFFIC_API_HOSTstringapi.apitraffic.ioHost the SDK loads its settings from.
ingestHostAPI_TRAFFIC_INGEST_HOSTstringingest.apitraffic.ioHost captured traffic is sent to.
accountSidAPI_TRAFFIC_ACCOUNT_SIDstringLearned from your accountOnly needed by a process that mints embed tokens without running the middleware.
captureSpansAPI_TRAFFIC_CAPTURE_SPANSswitchtrueMaster switch for spans, including the ones you record with span() and startSpan().
maxSpansPerRequestAPI_TRAFFIC_MAX_SPANS_PER_REQUESTcount500Spans kept per request.
spanSampleRateAPI_TRAFFIC_SPAN_SAMPLE_RATErate1Fraction of requests that keep full span detail.
maxSpanMetaBytesAPI_TRAFFIC_MAX_SPAN_META_BYTESbytes2048Size cap on each span's metadata, after redaction.
-API_TRAFFIC_EMBED_TOKENstring-An Administrator API token with no access scopes, used by embedToken(). See Embed tokens.

Delivery to ApiTraffic honours the standard HTTPS_PROXY, HTTP_PROXY, ALL_PROXY and NO_PROXY environment variables. Server labels (API_TRAFFIC_SERVICE_NAME, API_TRAFFIC_REGION and others), the remaining span options and the parsing rules are in the SDK configuration reference.

What gets captured

  • Every request that reaches the middleware, including ones whose controller throws (reported with the status Strapi's error handling sends) and ones the client abandons (reported as 499). Because Strapi's middlewares are global, that includes requests to the admin panel and its APIs (/admin, /content-manager, /upload and so on), not just /api. Use exclusions to drop the ones you do not want.
  • The request body as Strapi parsed it, for every method except GET and OPTIONS.
  • The response body as your controller set it - not a compressed form of it. A file stream (fs.createReadStream, as file downloads use) is recorded as a placeholder ([streamed body not captured]); other streamed responses (Server-Sent Events, ctx.respond = false) are captured from the bytes written, up to maxBodyBytes, and 204, 205, 304 and HEAD responses record no body.
  • The route Strapi matched, with its /api prefix, so GET /api/articles/42 is grouped as /api/articles/{id}.
  • Response time and time to first byte.
  • Outbound calls your Strapi code makes with http, https or fetch (unless interceptOutbound is off). See Outbound requests.
  • Each request once, even if the middleware is listed twice.

Bodies are capped at maxBodyBytes, and requestSampleRate sends a fraction of requests (server errors always). Bodies that cannot be captured safely are replaced with a [body not captured: …] placeholder and the rest of the request is still sent.

Data security

Data can be redacted before it leaves your server or once it arrives at ApiTraffic. Redaction settings need no code changes: they are configured in your ApiTraffic account and pushed down to every connected server, and nothing is sent before your account's client-side redaction rules have loaded. A request whose redaction cannot be completed is dropped, never sent unredacted. Learn more about redactions.

On top of your rules, credential headers are always masked, and credential-shaped body keys and query parameters are masked while redactSensitiveKeys is on (the default).

Exclusions stop requests that match a criteria - a health check, the admin panel - from being logged. Like redactions, they are configured in your account, with no change to your Strapi app. Learn more about exclusions.

Tags, traces and spans

The plugin exposes the request helpers of @apitraffic/node through its sdk service. Use them in controllers, services and policies - anything that runs while a request is being handled:

// src/api/article/controllers/article.js
const { factories } = require('@strapi/strapi');

module.exports = factories.createCoreController('api::article.article', ({ strapi }) => ({
  async find(ctx) {
    const apitraffic = strapi.plugin('apitraffic').service('sdk');

    apitraffic.tag('tenant', String(ctx.state.user?.tenantId ?? 'public'));
    apitraffic.trace('listing articles');
    return apitraffic.span('db.query', 'load articles', () => super.find(ctx));
  },
}));
HelperWhat it does
tag(key, value)Attach a searchable key-value pair to the request, for example a tenant or customer ID.
trace(message)Record a timestamped note on the request.
span(type, name, fn, meta?)Run fn, time it and record it as a span. Returns what fn returns and re-throws its error unchanged. time() is the same function.
startSpan(type, name, meta?)Start a span for work that does not fit one function; call end() on the handle (end({ ok: false }) marks it failed).
addSpan(span)Record a span you timed yourself.
getRequestManager() / getContext()The current request's context, or undefined outside a request. Rarely needed.
stats()Delivery counters: queue depth, drops by cause, circuit breaker state.
embedToken(options)Mint an embed token. See Embed tokens.
escapeCriteriaValue(value)Escape a value for a double-quoted embed criteria string.
flush() / shutdown()See Flushing and shutdown.
register(options?)Start APM in code. Usually too late inside Strapi; see APM.
utilitiesThe base client the middleware set up. Rarely needed.

Span type is one of db.query, redis, cache, http.out, queue, rpc, graphql or custom. Tags follow the tag rules: a key is 1 to 64 characters, a request holds at most 32 tag keys, and values are cut to 255 characters. A tag that breaks a rule is dropped and counted, never thrown.

Outside a request, and when capture is disabled, the helpers do nothing; tag() and trace() never throw into your code. The full reference is Per-request APIs.

TypeScript. The plugin ships no type declarations, so the service is untyped. Its helpers are @apitraffic/node's; to type them, add @apitraffic/node (^2) as a direct dependency and cast:

type ApiTrafficSdk = Pick<
  typeof import('@apitraffic/node'),
  'tag' | 'trace' | 'span' | 'time' | 'startSpan' | 'addSpan' | 'getContext' | 'getRequestManager' | 'stats' | 'embedToken' | 'escapeCriteriaValue' | 'flush' | 'shutdown' | 'register'
> & { utilities: unknown }; // the base client; rarely needed

const apitraffic = strapi.plugin('apitraffic').service('sdk') as unknown as ApiTrafficSdk;

Embed tokens

The embeddable request log authenticates with a short-lived token minted on your server. embedToken() mints it with an Administrator API token with no access scopes set as API_TRAFFIC_EMBED_TOKEN - the ingest token the plugin reports with cannot mint embed tokens. A token passed to embedToken() takes precedence.

// src/api/embed/controllers/embed.js
module.exports = ({ strapi }) => ({
  async token(ctx) {
    const apitraffic = strapi.plugin('apitraffic').service('sdk');

    const { token, expiresAt } = await apitraffic.embedToken({
      // Structured: the id is escaped, so it cannot add a condition of its own.
      filters: { tags: { customerId: String(ctx.state.user.id) } },
      ttl: 900,
    });

    ctx.body = { token, expiresAt };
  },
});

The host, bucket and account come from the SDK you already configured. This is the one helper that rejects rather than going quiet: a failure is a 500 on that route instead of a broken embed with nothing to explain it. The options are listed in Generate an embed token on your server.

API_TRAFFIC_EMBED_TOKEN is a read credential. An Administrator API token can mint embed tokens for any bucket in its account, with any filters or none, so whoever holds it can read all of the account's captured traffic. Set it only on servers that mint embed tokens, keep it in a secret store (never in client-side code, images or a repository), and rotate it immediately if it may have been exposed.

Flushing and shutdown

The plugin stops the SDK in Strapi's destroy phase: it delivers what was captured, waiting up to 2 seconds, then releases the realtime connection, timers and http/fetch hooks, and stops the APM tracing the preload started. The whole step gives up after 3 seconds and never throws, so Strapi's shutdown and strapi develop restarts are not held up. You do not need to call shutdown() yourself.

To deliver captured requests without stopping - before a process is frozen or recycled - call flush():

await strapi.plugin('apitraffic').service('sdk').flush({ timeoutMs: 1000 });

flush() waits at most timeoutMs (default 2 seconds) and resolves with { flushed, remaining }; anything not delivered stays queued. shutdown({ flushTimeoutMs }) resolves the same way. Neither throws. See Shutting down.

Firewall

The ApiTraffic web application firewall is part of the plugin's middleware, and whether it runs is decided in your ApiTraffic account: the engine is not loaded until a policy for the bucket is switched on, and a policy change reaches every server without a deploy. Until then it costs one check per request.

  • It runs right after capture, so with the middleware after strapi::body its body rules see the parsed body, and ahead of Strapi's router, so a request it blocks never reaches a route policy or a controller.
  • A blocked request is still recorded, with the policy's status, and tagged waf.*.
  • IP lists, rate limits, country lists and IP reputation use the client address Koa reports. Behind a load balancer or proxy, set proxy: { koa: true } in config/server.js so Strapi trusts it; otherwise every request has the proxy's address.

To refuse it locally, whatever the dashboard says (change control, or an incident switch that does not depend on the control plane), set waf: false in the plugin configuration or API_TRAFFIC_WAF_ENABLED=false:

// config/plugins.js
module.exports = () => ({
  apitraffic: {
    config: { waf: false },
  },
});

Strapi's middlewares are global, so the firewall also checks requests to the admin panel and its APIs (/admin, /content-manager, /upload and so on). Rich-text fields saved through the Content Manager can hold HTML that cross-site scripting rules match. Start a new policy in Monitor, and if editors' saves are flagged, add a false positive exclusion for the xss category with the path pattern ^/content-manager/ and the target body, rather than switching protection off.

See Set up the firewall for policies and rules.

APM

The APM bridge records the database, cache, queue and fetch calls made while each request is served as spans on that request. Install @apitraffic/apm, which brings the OpenTelemetry packages it needs:

npm install @apitraffic/apm

OpenTelemetry patches modules as they are loaded, so it has to start before Strapi loads its database driver; the plugin itself starts too late. Preload it in the start script of your package.json, so it runs in the Strapi process and not in npm's:

{
  "scripts": {
    "start": "NODE_OPTIONS='--require @apitraffic/strapi/register' strapi start"
  }
}

On Windows, use cross-env to set NODE_OPTIONS. If you start Strapi from your own script, node -r @apitraffic/strapi/register server.js does the same. @apitraffic/strapi/register is @apitraffic/node/register, re-exported so it resolves from an app that installs only this plugin.

The preload reads its options from the environment before Strapi loads .env, so set APM options such as API_TRAFFIC_CAPTURE_STATEMENTS in the real environment. Strapi's destroy phase stops APM with the rest of the SDK, for the rest of that process. The preload never throws: when it cannot start (no OpenTelemetry packages, another APM agent, an existing OpenTelemetry provider) it says why once and Strapi starts as usual. If your app already configures OpenTelemetry, add ApiTrafficSpanProcessor from @apitraffic/apm to that provider instead. See Set up APM.

Failure behaviour

The plugin is an observer of your app, not part of it, so it can never be the reason Strapi fails to start or serve.

  • Missing token or bucket. Strapi logs [apitraffic] token (API_TRAFFIC_TOKEN) and bucket (API_TRAFFIC_BUCKET) not set; request capture is disabled. (or just the one that is missing) at startup, and the middleware passes every request through untouched.
  • @apitraffic/node cannot be loaded. The plugin logs [apitraffic] could not load @apitraffic/node; request capture is disabled: … and the middleware passes requests through.
  • The firewall engine cannot be loaded. The firewall stays off with a logged reason, and capture carries on.
  • ApiTraffic unreachable. Requests are served normally. A short outage is absorbed by an in-memory retry queue; a longer one sheds data rather than growing without bound. See If ApiTraffic is unreachable.
  • Realtime connection fails. You lose live configuration updates and nothing else.
  • Instrumentation errors. tag() and trace() never throw into your handlers, and an error in the per-request setup falls through to your app with no capture for that request.

strict: true makes the SDK throw on misconfiguration rather than log. With the plugin, a missing token or bucket never reaches the SDK (the middleware is a pass-through first), and the SDK starts without being awaited, so strict does not stop Strapi from booting.

Troubleshooting

Nothing is captured after installing the plugin

  • Check that plugin::apitraffic.apitraffic is in config/middlewares.js.
  • Look for [apitraffic] … not set; request capture is disabled. in the Strapi log: the token or bucket did not reach the process.
  • Restart Strapi after changing .env or the configuration files.
  • A middleware listed before ApiTraffic may have answered the request (see Where to put the middleware), or an exclusion may match it.
  • Set API_TRAFFIC_DEBUG=true (or debug: true in the plugin configuration) and restart to see what the SDK is doing.

The log says the token was rejected

ApiTraffic: token rejected (HTTP 401) or (HTTP 403) means the token is invalid or cannot access the bucket in API_TRAFFIC_BUCKET. Check both values; a token belongs to one environment. See API tokens.

Routes are grouped by raw path

Update the plugin to the latest version: older builds send the raw path.

The request body is missing

Make sure strapi::body is still in config/middlewares.js; it is what parses request bodies. GET and OPTIONS request bodies are never captured.

Requests are not blocked

Check that a firewall policy is enabled for this bucket and environment in Enforcing mode, and that neither waf: false nor API_TRAFFIC_WAF_ENABLED=false is set. A middleware listed before the plugin that answers a request (static files, CORS preflights) answers it before the firewall sees it.

Requests have no APM spans

Check that Strapi was started with the @apitraffic/strapi/register preload and that @apitraffic/apm is installed. The preload prints why when it does not start.

Outbound calls are missing

Check that interceptOutbound / API_TRAFFIC_INTERCEPT_OUTBOUND is not off or blank, and see When an outbound call is not captured.

Your network needs a proxy or firewall rule

Allow outbound HTTPS to ingest.apitraffic.io and api.apitraffic.io, or set HTTPS_PROXY (and NO_PROXY as needed) for the Strapi process.

More in the general troubleshooting guide.

Documentation

Related packages:

  • @apitraffic/node - the whole SDK, which this plugin is built on, for Express, Fastify, Koa and hapi.

Package: @apitraffic/strapi on npm.

Contributing, changelog and license

  • Questions, bug reports and feature requests: email support@apitraffic.io or ask on Discord.
  • Release notes are in the changelog, shipped with the package from 0.2.2. A release that only follows a new @apitraffic/node range may have no entry of its own; every published version is listed on npm.
  • Licensed under the MIT License.

Community

Follow @apitraffic/strapi on npm to stay updated on new releases.

Follow us on our Blog and on X.

Chat with the ApiTraffic team and other members on Discord and follow our tutorials and other videos on YouTube.

ApiTraffic Discord

ApiTraffic YouTube

ApiTraffic on X

How to contribute

Here are some ways you can help improve ApiTraffic:

  • Give ApiTraffic a try and share your feedback to help make ApiTraffic even better for you. Feel free to reach out to us on Discord!
  • Join our Discord to connect with other members, share your experiences, and learn from one another.
  • Report bugs or request features by emailing support@apitraffic.io or asking on Discord.

Keywords

strapi

FAQs

Package last updated on 03 Nov 2026

Related posts