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:
| Slug | Name | Default | Production | Colour |
|---|---|---|---|---|
dev | Development | yes | #6366f1 | |
staging | Staging | #f59e0b | ||
prod | Production | yes | #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
| Method | Path | Role |
|---|---|---|
GET | /environments | User |
POST | /environments | Admin |
PATCH | /environments/{id} | Admin |
POST | /environments/{id}/promote-to-default | Admin |
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 byENVIRONMENT_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 state | Override sent | Result | x-axonpush-resolved-via |
|---|---|---|---|
| Pinned, override not allowed | none, or same slug | The key’s environment | apiKey |
| Pinned, override not allowed | a different slug | 400 env_override_forbidden | |
| Pinned, override allowed | a slug | The requested environment | override |
| Not pinned | a slug | The requested environment | overrideOnUnscopedKey |
| Not pinned | none | The org default | orgDefault |
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: stagingHonoured 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: overridecurl -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-resolvedChoosing 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=prodExport 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.
Related
- OTLP ingest, the
X-Axonpush-Environmentheader on OTLP - Sentry DSN ingest, where the
environmentfield becomes an override - Audit log, what is and is not recorded
Authentication
Two planes of auth, Better Auth JWTs for the dashboard and MCP, and ak_ API keys plus pt_ public ingest tokens for programmatic access. What each is for, and how the Go API verifies them.
OpenTelemetry over OTLP
OTLP/HTTP ingest in both JSON and protobuf, how a request is routed to a channel without a channel header, and how to forward the same events back out to a downstream collector.