axonpush
Concepts

Environments

How dev, staging and prod isolation works, where an environment lives, how ingest resolves one per request, and what happens when a request asks for an environment its credential is not allowed to write to.

An environment is a label on an organisation that every stored event carries. It is the axis the dashboard, analytics, alerts and export destinations filter on, and the axis an API key is pinned to.

Environments belong to the organisation, not to an app. One prod environment covers every app in the org, so a key for app A and a key for app B can both be pinned to the same prod and their traces sit in the same production view.

Seeded environments

The first time an organisation’s environment list is read, three are created:

SlugNameDefaultProductionColour
devDevelopmentyes#6366f1
stagingStaging#f59e0b
prodProductionyes#ef4444

dev is the default, an unpinned credential writes there, not to production. Promote a different one with POST /environments/{id}/promote-to-default when you want that fallback to change.

prod is created with requireConfirmationForDestructive: true, which is what makes the dashboard demand a typed confirmation before a destructive action. The flag is copied from isProduction at creation time and can be changed afterwards with PATCH /environments/{id}.

Managing them

MethodPathRole
GET/environmentsUser
POST/environmentsAdmin
PATCH/environments/{id}Admin
POST/environments/{id}/promote-to-defaultAdmin
DELETE/environments/{id}Admin

POST /environments takes name and optionally slug, color, isProduction, isDefault and cloneFromEnvId. When slug is omitted it is derived from name by lowercasing and replacing runs of non-alphanumerics with hyphens. A slug must match ^[a-z0-9][a-z0-9-]{0,39}$ and must not start with axonpush-. When isProduction is omitted it is inferred: a slug of production or prod is treated as production.

Two failures are worth knowing about, because both return 409 with a machine readable code:

  • slug_taken, that slug already exists in the organisation.
  • env_cap_exceeded, the organisation is at its environment cap. The cap defaults to 20 and is set per deployment by ENVIRONMENT_SOFT_CAP.

DELETE is a soft delete, and it refuses with 400 if the target is the default environment. Promote another one first.

Environment changes are not written to the audit log.

The whole /environments surface is behind the environments feature flag. On a deployment where it is off, every route on this page returns 404, and trace queries silently ignore an environment filter rather than failing. GET /capabilities does not report this flag; a 404 from GET /environments is the signal.

How ingest picks an environment

Every ingest request resolves exactly one environment before anything is written. The inputs are the credential and, optionally, an override.

Public ingest tokens (pt_*) carry an environmentId and that is final. An override is not consulted. A token with no environment is a 400.

API keys (ak_*) may or may not be pinned to an environment, and may or may not carry allowEnvironmentOverride. The four cases:

Key stateOverride sentResultx-axonpush-resolved-via
Pinned, override not allowednone, or same slugThe key’s environmentapiKey
Pinned, override not alloweda different slug400 env_override_forbidden
Pinned, override alloweda slugThe requested environmentoverride
Not pinneda slugThe requested environmentoverrideOnUnscopedKey
Not pinnednoneThe org defaultorgDefault

The third row is the one that bites. A pinned key does not quietly ignore an unwanted override, it rejects the request. That matters most for Sentry ingest, where the environment field on the event is the override and your Sentry SDK may be sending it without your having thought about it.

An unknown slug is also a 400, with a response body that tells you what does exist:

{
  "code": "unknown_environment",
  "message": "Unknown environment slug \"prod-eu\"",
  "knownEnvs": ["dev", "staging", "prod"],
  "hint": "create it at POST /environments"
}

The override header

X-Axonpush-Environment: staging

Honoured on POST /v1/logs, POST /v1/traces and the Sentry routes under /api/{channelId}/. The value is an environment slug, not an ID.

Every ingest response carries the resolution back, so you can confirm where your data landed without opening the dashboard:

x-axonpush-resolved-environment: staging
x-axonpush-resolved-via: override
curl -si -X POST https://api.axonpush.xyz/v1/logs \
  -H "X-API-Key: $AXONPUSH_API_KEY" \
  -H "X-Axonpush-Environment: staging" \
  -H "Content-Type: application/json" \
  -d '{"resourceLogs":[]}' | grep -i x-axonpush-resolved

Choosing a strategy

One key per deployment target is the common shape: mint a key pinned to prod, another pinned to dev, leave allowEnvironmentOverride off, and let your secret manager decide which one a given process gets. Nothing in the application chooses an environment, so nothing in the application can get it wrong.

One key with override suits a build system or a test harness that needs to write into several environments in one process. Set allowEnvironmentOverride: true on the key and pass X-Axonpush-Environment per request. You have traded a config-level guarantee for a code-level one.

Public tokens (pt_*) are for browser and mobile code, where shipping an ak_* key would hand it to every user. A public token is minted against one channel and one environment via POST /public-tokens, and no header can move it. Browser code cannot write to production by accident because it cannot address production at all.

Reading by environment

Environment is a filter, not a partition, the read APIs take an environment query parameter carrying a slug:

GET /v2/traces?environment=prod
GET /events/search?environment=prod
GET /v2/analytics/timeseries?environment=prod

Export destinations are scoped by envSlug, so forwarding production traffic to a downstream collector while leaving dev alone is a property of the destination rather than something you filter after the fact. Alert rules take an optional environmentId, which is the environment’s ID rather than its slug.