Nexmap documentation
Administrators
Deploying, configuring and running a Nexmap server.
- Installation: container image, Coolify, first start
- Configuration: every environment variable and flag of
nexmapd - Security: TLS, the encryption key, the first administrator, accounts and tokens
- Backup and restore: what to back up, SQLite and PostgreSQL, restoring, upgrades
Users
Working with Nexmap in the browser.
- Concepts: products, structure, requirements, tasks, constraints, defects, tests, roles
- Getting started: creating a product, settings, team, structure
- Backlog: requirements, tasks, constraints, defects, boards and sprints
- Testing: test cases, campaigns, recording results
CLI users
Developers, CI pipelines and AI agents working with the nexmap command line client.
- Setup: installing, logging in, connecting a repository, configuration, commands
- Scripting: output formats, exit codes, raw API calls
- CI integration: recording automated test results with
nexmap utest - AI agents: agent guide, MCP server, what agents may change
API
- REST API: authentication, response format, endpoints
Contributing
The source repository holds the material for contributors that this documentation leaves out:
docs/spec/, the specification: what Nexmap shall do, with requirement identifiers. It describes targets, not necessarily the current behaviour;docs/decisions/, the design decisions and their reasoning;CONTRIBUTING.md: building, testing, and how this documentation is organised and published.
Concepts
Nexmap keeps everything about a product in one place: what it is made of, what it must do, the work to get there, how it is tested and what is broken. This page explains the building blocks and how they relate.
Products
Everything in Nexmap belongs to a product. Each product has its own team, settings, structure, backlog and tests; nothing is shared between products. The dashboard shows the products you are a member of, and the product switcher in the header moves between them while staying in the same section.
Structure: components and capabilities
- Components are the parts a product is built from, such as the server, the mobile app or the billing module.
- Capabilities are what the product can do, such as “export reports” or “sign in with 2FA”. A capability belongs to a component, or directly to the product if it spans several.
Structure is optional, but it is what everything else hangs off: requirements, constraints, test cases and defects can all be linked to capabilities, which tells you, per feature area, what is required, how it is tested and what is broken.
Documents
A document is a longer text that defines part of the product: a protocol, a data schema, a file
format, an interface contract. It belongs to the product, or to one of its components or capabilities,
and has an identifier like DOC-3, from a counter of its own (the prefix DOC can be changed under
Settings → General).
- Format. A document is written in Markdown, or kept verbatim in a machine-readable format such as JSON, YAML, XML, Protocol Buffers, SQL or GraphQL. Verbatim documents are shown and exported exactly as written.
- State. Only current documents are binding. A draft is work in progress; a superseded document is history and can name the document that replaced it.
- Revisions. Every change to a document’s title, content or format is kept as a revision. Any two revisions can be compared, and an earlier one can be made current again; that becomes a new revision, so nothing is lost.
- Links. Requirements that introduce or change a document, constraints that require conformance to it, and test cases that verify it are linked to it. Each of them lists its documents.
Nexmap is where documents are written. A repository gets copies through the specification export, never the other way round, so someone (or an AI agent) with access to Nexmap has everything needed to implement the product or check code against it.
Requirements, tasks and constraints
These three look alike, but they are different things:
| Requirement | Task | Constraint | |
|---|---|---|---|
| What it is | A statement of what the product must do | Work that says nothing about the product: maintenance, tooling, research | A rule the product must keep following |
| Example | “Users can export a report as PDF” | “Upgrade the PDF library” | “All traffic is encrypted with TLS” |
| Done when | Its definition of done holds | It is finished | Never: it is met or violated, continuously |
| Moves through the workflow | Yes | Yes | No; it has a compliance state instead |
| Linked to capabilities and test cases | Yes | No | Capabilities only |
Each one gets an identifier from its category: FR-12 is the 12th item in the product’s
numbering, in the category with prefix FR. Requirements and tasks share one counter, so FR-12 and
MT-12 can never both exist. Use identifiers in conversation, commit messages and code comments.
New products start with these categories, which the product owner can change:
- Requirements: Functionality (
FR), Architecture (AR), Security (SR), UX (UR) - Tasks: Maintenance (
MT), Tooling (TL), Spike (SP) - Constraints: Architecture (
AC), Security (SC), UX (UC)
Requirements and tasks can be broken down: a requirement can have child requirements or tasks, and a task can have child tasks. A requirement never sits below a task. Items that still need breaking down can be marked coarse.
Together, the open requirements and tasks are the product’s backlog. See Backlog.
Defects
A defect is something that does not work as it should. It has a criticality (low, medium, high, critical), moves through the same kind of workflow as the backlog, and can be linked to the components, capabilities, requirements and test cases it affects.
Defects are numbered by a counter of their own and get identifiers like DF-17. The prefix DF can be
changed under Settings → General; it must differ from every category prefix.
Test cases and testing campaigns
- A test case describes one check. It is manual (a person carries it out) or automatic (a test in code, identified by its test identifier, such as a Go test function name). It can run in several runtime environments, such as operating systems or browsers, and sit at a level of the test pyramid (by default Unit, Integration, End-to-End).
- A testing campaign is one round of testing: a selection of test cases, each with a result per environment. Results start as pending and become passed, failed, blocked, skipped or re-test. A failed result always names the defect that explains it.
See Testing.
Milestones and sprints
- Milestones mark points on the product’s roadmap. Their type says what they are: planning, release or internal. Components, capabilities, requirements, constraints, defects and sprints can be assigned to a milestone; an item belongs to at most one.
- Sprints are time boxes of work, used when a product works in scrum mode. See Backlog.
Comments, attachments and history
Requirements, tasks, constraints, documents, defects, test cases, components, capabilities and milestones all have comments and attachments, and every change to them is recorded in their History tab: who changed what, and when. Descriptions and comments are written in Markdown.
Roles
What you can do in a product depends on your role in it. Each role includes everything the roles below it can do:
| Role | Can |
|---|---|
| Product owner | Everything in the product: settings, team, structure, documents, backlog, campaigns |
| Developer | Create and edit test cases; comment; change the status of requirements, tasks and defects once they are activated |
| Tester | Record campaign results; create defects; comment on defects and test cases |
| Viewer | Read everything, change nothing |
Administrators manage the whole installation (users and instance settings) and can act in every product.
Getting started
This page walks a product owner through setting up a product: creating it, adjusting its settings, bringing in the team and describing its structure. The terms are explained in Concepts.
Create a product
On the dashboard, choose Create Product (/products/new). Any user can do this if the administrator has
enabled product creation; administrators always can. You become the product’s owner.
| Field | Meaning |
|---|---|
| Name, description | Shown on the dashboard and the product overview |
| Estimation mode | How backlog items are estimated: story points, t-shirt sizes, hours, or disabled |
| Workflow mode | Kanban (one continuous board) or scrum (work planned into sprints); see Backlog |
Adjust the settings
Settings in the product’s sidebar has five tabs. Changes are saved as you make them.
- General: name, description, estimation mode, workflow mode, the defect identifier prefix
(
DFinDF-17), the document identifier prefix (DOCinDOC-3), and whether members must use two-factor authentication. With that switch on, members who have not set up 2FA cannot open the product until they do; the Team page marks them. You need 2FA on your own account to turn it on. - Categories: the categories of requirements, tasks and constraints, each with a name and an
identifier prefix. The defaults are listed in Concepts.
Settle the prefixes early, before identifiers like
FR-12appear in commit messages. - Statuses: the workflow columns. New products start with New, In Progress, In Review and Done. One status is the default for new items; statuses marked closed (like Done) count as finished, so their items drop out of “open” lists. In kanban mode, statuses marked active (at first In Progress and In Review) are where developers may work on items; see Activation. At first, requirements, tasks and defects share one set of statuses. If defects need a different workflow, split the sets; splitting cannot be undone.
- Environments: the runtime environments tests run in, such as
linux,windowsorfirefox. Name them the way your CI jobs will (see CI integration). - Pyramid levels: the levels of the test pyramid, by default Unit, Integration and End-to-End.
Bring in the team
On the Team page, search for existing users, add them with a role, and change or remove roles later. The roles are explained in Concepts.
People who have no account yet can be invited:
-
Under Invite New User, enter their email address and the role they should get.
-
If your installation sends email, choose how the invitation gets there:
- Send the invitation by email: Nexmap mails the link to that address. You do not see the link yourself, so only the owner of the mailbox can create the account.
- Create a link to pass on myself: Nexmap shows the link, and you send it through a chat, a forum message or any other channel only that person reads: whoever opens it can use it.
Without email, Nexmap always shows the link.
-
Choose Send Invitation.
-
The link opens a page where they choose a username and password. Their account gets the invited email address, joins the product with the invited role, and they are signed in right away.
An invitation link works once and expires after 7 days. Accounts created from an invitation never need an administrator’s approval.
If the administrator has enabled self-registration, people can also create an account themselves on the
sign-in page (Register) and then be added to the product by you. With moderation enabled, such an
account waits for an administrator’s approval under /admin/users before it can sign in.
To hand the product over, use Transfer Ownership next to a member. They become the owner and you become a developer.
Describe the structure
Under Structure in the sidebar:
- Create the components the product consists of.
- Create capabilities, each under a component or at product level.
- Optionally, lay out the roadmap as milestones.
The structure can grow over time; nothing else depends on it being complete.
Fill the backlog and the test catalogue
- Write down the rules that always apply as constraints, and what the product must do as requirements, each with a definition of done. Link both to the capabilities they concern. See Backlog.
- Describe how requirements are verified as test cases, linked to the requirements they cover. For
automated tests,
nexmap test synccan create the test cases from a test run instead; see CI integration. - When it’s time to test, run a testing campaign; see Testing.
Your own account
Under your user menu:
- Profile (
/user/profile): display name, email address, password, theme, two-factor authentication, and a request to delete your account. - API Tokens (
/user/tokens): personal access tokens for the CLI.
Backlog
The backlog holds the product’s requirements, tasks and defects, and the boards and sprints you work them off with. Constraints live next to them. The differences between these are explained in Concepts.
Requirements and tasks
Requirements and Tasks in the sidebar list them separately. Product owners create and edit them.
| Field | Meaning |
|---|---|
| Category | Decides the identifier prefix, such as FR or MT |
| Title, description | The description is written in Markdown |
| Definition of done | Requirements only: when the requirement counts as fulfilled. Test cases can be linked to it |
| Parent | Breaks work down: a requirement under a requirement, a task under either |
| Status | Position in the workflow; new items get the product’s default status |
| Milestone | The milestone this item contributes to |
| Sprint | Scrum mode only: the sprint the item is planned into |
| Priority | A number; lower numbers mean higher priority |
| Estimate | Only when the product uses estimation, in the product’s unit |
| Coarse | Marks an item that still needs breaking down |
A requirement’s detail page shows its links in tabs: the capabilities it concerns, the test cases that verify it, and the defects reported against it. Tasks have no such tabs, because they make no statement about the product to verify.
Constraints
Constraints are rules that always apply, such as “all traffic is encrypted”. Instead of a workflow status, each has a compliance state: not assessed, met or violated. Review constraints when designing a change, and set a constraint to violated when the product breaks it, so the problem stays visible until it is fixed. Constraints can be linked to capabilities and assigned to a milestone.
Defects
Defects records what is broken. Testers, developers and product owners can report defects, testers usually from a failed test result (see Testing). Editing a defect’s fields is up to product owners; developers change its status (see Activation).
A defect has a criticality (low, medium, high, critical), a status, optionally the runtime environment it occurs in, and links to the components, capabilities, requirements and test cases it affects. From a defect’s Test Cases tab you can link an existing test case or create a regression test for it.
Moving a defect into a closed status (such as Done) puts every failed result it explains back to re-test in its testing campaign; see Testing.
Working with lists
All lists work the same way:
- The search box matches the text columns; filters narrow down columns with a fixed set of values, such as status or category. A single control resets search and filters.
- Clicking a column header sorts ascending, then descending, then back to the default order. Statuses sort in workflow order and criticalities by severity.
- Search, filters, sorting and the page are part of the URL, so a filtered view can be bookmarked or shared.
Kanban or scrum
The product’s workflow mode decides how work is organised.
Kanban
Board shows all requirements, tasks and defects in one column per status. Drag a card to another column to change its status. If the product’s statuses have been split, requirements and defects get separate boards. Defects also has a board of its own. Columns marked Active hold the work that is activated for developers; see Activation for who may move what.
Scrum
Work is planned into sprints, each with a start and end date and optionally a milestone it leads to. A sprint’s detail page has four tabs:
- Board: the sprint’s items in status columns, as on the kanban board.
- Planning: the product backlog next to the sprint’s contents; drag items between them. Only product owners can plan.
- Summary: how many items are in which status, progress, and estimate totals.
- History: the sprint’s changes.
A sprint is planned until its owner chooses Start Sprint, and completed after Close Sprint. Only one sprint per product can be active at a time. Closing a sprint does not move unfinished items; plan them into the next sprint.
Activation: what developers may move
Developers may change the status of a requirement, task or defect only once a product owner has activated it. This keeps the product owner in charge of what gets worked on. Only product owners activate items, and they can always change statuses.
- In kanban mode, activation belongs to the board’s columns. Under Settings → Statuses, each column can be marked Active; the board shows those columns with an Active badge. An item is active while it sits in an active column. A product owner activates an item by moving it into an active column, and from there developers can move it to any column, including back to an inactive one such as Done. Items in inactive columns show a lock to developers and cannot be dragged by them. A new product starts with New and Done inactive and In Progress and In Review active.
- In scrum mode, planning an item into a sprint activates it, and taking it out of the sprint deactivates it again. A product owner can also activate an item directly through the API. The column flags are not used. Switching a product from kanban to scrum resets activation to the sprint plan.
The detail page shows whether an item is active.
Testing
Testing in Nexmap has two parts: a catalogue of test cases that describes how the product is verified, and testing campaigns that record the results of one round of testing.
Test cases
Test Cases in the sidebar lists the catalogue. Developers and product owners create and edit them.
| Field | Meaning |
|---|---|
| Title, description | What is checked and how; for manual tests, the steps to follow |
| Execution mode | Manual (a person carries it out) or automatic (a test in code) |
| Test identifier | Automatic tests only: the name that test results report, such as a Go test function name. It connects CI results to the test case |
| Component, capability | What part of the product the test case covers |
| Pyramid level | Unit, Integration, End-to-End, or the product’s own levels |
| Environments | The runtime environments the test runs in. A campaign tracks one result per selected environment; with none selected, there is a single result |
On a test case’s Requirements tab, link the requirements it verifies, either the requirement as a whole or its definition of done. The Campaigns tab shows how the test case fared across campaigns.
Automated tests can be registered in bulk from a test run with nexmap test sync; see
CI integration.
Testing campaigns
A campaign moves through three states:
- Draft: a product owner creates it under Campaigns with a name, an optional description and planned dates, and selects the test cases in scope. Filters for component, capability, pyramid level and execution mode, plus a select-all over the filtered list, help to pick them. The scope can still change while the campaign is a draft.
- Active: the product owner activates it. The scope is now fixed: test cases created later do not join. Results can only be recorded while a campaign is active, and only one campaign per product can be active at a time.
- Closed: the product owner closes it, which freezes all results. Closing a campaign with results that are still pending, blocked or awaiting re-test asks for confirmation first.
Recording results
The campaign’s Matrix tab shows test cases in rows and environments in columns, with each result’s status in its cell. Rows can be grouped by component or by pyramid level. Testers, developers and product owners click a cell to set its result:
| Status | Meaning |
|---|---|
| Pending | Not run yet; every result starts here |
| Passed | The check succeeded |
| Failed | The check failed; requires a defect |
| Blocked | The check could not be carried out, for example because something it depends on is broken |
| Skipped | Deliberately not run in this campaign |
| Re-test | The defect behind a failure was fixed; run the check again |
A small amber dot on a cell marks a result whose status has changed more than once in this campaign.
Marking a result as failed always requires a defect that explains it: link an existing one, or create one on the spot. The defect is linked to the test case automatically.
Automated tests record their results from CI instead; see CI integration.
The defect loop
When a defect that explains failed results is moved into a closed status, those results move to re-test. They are not marked as passed automatically: a tester runs the check again and records the outcome. For automated tests, the next green CI run does this.
Progress and verdict
The Progress tab shows how many results are in which status and how that changed over time; the History tab lists every result change with who made it and when.
A campaign is complete when no result is pending, blocked or awaiting re-test. It passes when every result that was not skipped has passed.
Installation
Which version to deploy
Deploy the branch stable or a release tag, never main:
| Ref | Holds | Use it for |
|---|---|---|
stable | The newest release; it moves only when a release is made | Deployments that update with each release, such as Coolify watching the branch |
v0.9.1, … | One release, unchanged | Deployments pinned to a version, updated by hand |
main | Development work between releases, reporting v0.9.1+dev | Development only |
Pre-releases (v1.0.0-rc.1) and fix releases for older versions are tagged, but do not move stable;
deploy them by their tag. Updating to a new release runs its database migrations, which cannot be
undone, so back up before a release reaches your instance.
Container image
The Dockerfile’s default stage is the server image: nexmapd alone on a distroless base, running as a
non-root user, listening on port 8080 on all interfaces, with a HEALTHCHECK (nexmapd healthcheck).
It keeps the SQLite database and attachments in /data, so mount persistent storage there. Migrations
run automatically when the server starts.
When building with podman, pass --format docker: the default OCI image format drops the HEALTHCHECK.
Coolify
Create the application with the Dockerfile build pack and:
- set the branch to
stable(see which version to deploy). With automatic deployment on, every release then updates the instance, and nothing else does; - add persistent storage (a volume) with destination path
/data; - set the environment variables, at least
NEXMAP_ENCRYPTION_KEY(see generating a key), andNEXMAP_ADMIN_USER/NEXMAP_ADMIN_PASSWORDfor the first start;NEXMAP_PUBLIC_URLwith the application’s domain; for PostgreSQL,NEXMAP_DB_DRIVER=postgresandNEXMAP_DB_DSN. All variables are listed in the configuration reference; - nothing for the version: it comes from the source tree (
internal/buildinfo/VERSION). A release reports its version (v0.9.1), a build between releases the release it is based on and, when Coolify passesSOURCE_COMMITto the build, the commit (v0.9.1+dev.<commit>). Do not set aVERSIONvariable: Coolify passes it to the build, where it overrides the version from the source.
Ports (8080) and the health check come from the image. Leave Coolify’s own health check (in the
application’s Healthcheck settings) turned off: Coolify runs it through /bin/sh and, for the HTTP
type, curl or wget inside the container, and the distroless image has none of them, so it fails
with stat /bin/sh: no such file or directory. The image’s HEALTHCHECK runs nexmapd healthcheck
directly and needs no shell. Coolify’s proxy terminates TLS; see
Security for what the server expects from it.
After the first start
Sign in as the administrator created from NEXMAP_ADMIN_USER / NEXMAP_ADMIN_PASSWORD (or with the
generated password from the log, see the first administrator).
Then change the administrator’s email address on the profile page, and review the instance settings under
/admin/settings: self-registration, moderation, product creation, enforced 2FA and password rules.
Nexmap makes no backups itself: set them up before relying on the instance, see Backup and restore.
Server configuration
nexmapd is configured through environment variables, most of which also have a command line flag.
Each setting is taken from the first source that has it: flag, then environment variable, then the
built-in default. There is no configuration file.
The container image sets some variables differently from the built-in defaults; both are listed below.
Reference
Network
| Variable | Flag | Default | Image | Meaning |
|---|---|---|---|---|
NEXMAP_HOST | --host | localhost | 0.0.0.0 | Address to listen on. 0.0.0.0 listens on all interfaces |
NEXMAP_PORT | --port | 8080 | 8080 | Port to listen on |
NEXMAP_PUBLIC_URL | --public-url | empty | The address users open Nexmap at, such as https://nexmap.example.com. Used for the links Nexmap hands out (invitations, password resets). Required when mail is enabled | |
NEXMAP_TRUSTED_PROXIES | --trusted-proxies | loopback and private networks | Addresses and CIDR ranges of the reverse proxies whose X-Forwarded-For header is believed, comma-separated. none (or empty) trusts no proxy |
Without NEXMAP_PUBLIC_URL, invitation links are built from the address the inviter’s browser used,
taking the scheme from the proxy’s X-Forwarded-Proto header. That is right behind a typical proxy, but
set the variable if Nexmap is reachable under more than one name, or if links come out with http://.
The server does not start if the value is not an absolute http or https URL.
NEXMAP_TRUSTED_PROXIES decides whose word Nexmap takes for a client’s address, which the login limits
count by. The default (127.0.0.0/8, ::1, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
fits a proxy on the same host or container network. Narrow it to your proxy’s address if other machines
on those networks can reach nexmapd directly. The server does not start if an entry is not an IP
address or CIDR range. See Security.
nexmapd serves plain HTTP. Put it behind a reverse proxy that terminates TLS; see
Security.
Database
| Variable | Flag | Default | Image | Meaning |
|---|---|---|---|---|
NEXMAP_DB_DRIVER | --db-driver | sqlite | sqlite | sqlite or postgres |
NEXMAP_DB_DSN | --db-dsn | nexmap.db | /data/nexmap.db | For SQLite, the database file path. For PostgreSQL, a connection URL (postgres://user:password@host:5432/nexmap?sslmode=require) or a key/value string (host=… user=… dbname=…) |
SQLite suits small installations: one file, no separate server, run in WAL mode with a single connection.
PostgreSQL suits larger ones; nexmapd opens up to 25 connections. Either way, the database is created
or migrated automatically at startup, and the server refuses to start if it cannot reach it.
Encryption
| Variable | Flag | Default | Meaning |
|---|---|---|---|
NEXMAP_ENCRYPTION_KEY | — | none, required | Key ring for encrypting personal data, as JSON. See Security |
The server does not start without it.
First administrator
| Variable | Flag | Default | Meaning |
|---|---|---|---|
NEXMAP_ADMIN_USER | — | admin | Username of the administrator created on first start |
NEXMAP_ADMIN_PASSWORD | — | generated | Password of that administrator |
These are only read when the database has no users yet, i.e. on the very first start. See Security.
Sessions and passwords
| Variable | Flag | Default | Meaning |
|---|---|---|---|
NEXMAP_SESSION_TTL | --session-ttl | 24h | How long a web UI session lasts, as a Go duration (8h, 30m, 168h) |
NEXMAP_COOKIE_SECURE | --cookie-secure | true | Mark session cookies Secure, so browsers send them over HTTPS only. Set to false only for local development over plain HTTP |
NEXMAP_PBKDF2_ITERATIONS | --pbkdf2-iterations | 600000 | PBKDF2-SHA256 iterations for password hashes |
Each password hash records its own iteration count, so changing NEXMAP_PBKDF2_ITERATIONS applies to
passwords set from then on; existing passwords keep working.
Attachments
| Variable | Flag | Default | Image | Meaning |
|---|---|---|---|---|
NEXMAP_UPLOAD_DIR | --upload-dir | ./uploads | /data/uploads | Directory for uploaded attachments |
NEXMAP_MAX_UPLOAD_BYTES | --max-upload-bytes | 10485760 (10 MiB) | Largest accepted attachment, in bytes | |
NEXMAP_ALLOWED_UPLOAD_TYPES | --allowed-upload-types | empty: all types | Comma-separated presets and/or file extensions |
The presets are images (.png, .jpg, .jpeg, .gif, .webp), logs (.txt, .log, .json,
.xml, .csv, .md, .yaml, .yml) and documents (.pdf). For example,
NEXMAP_ALLOWED_UPLOAD_TYPES=images,logs,zip allows those presets plus .zip files.
Besides the extension, the server checks the file content: a file named .png must really be an image,
and a text extension rejects content recognised as an archive, PDF or media file. This stops mislabelled
uploads but is not a malware scanner. Attachments are always served as downloads, never rendered or
executed by the browser.
Nexmap can send two kinds of email, and no others: invitations and password reset links. Mail is optional. Without it, invitations are links that the inviter passes on, and an administrator sets a new password for someone who forgot theirs.
| Variable | Default | Meaning |
|---|---|---|
NEXMAP_MAIL_TRANSPORT | empty: no mail | smtp, mailgun, sendgrid, mailpace, scaleway or lettermint |
NEXMAP_MAIL_FROM | none, required with a transport | Sender, such as nexmap@example.com or "Nexmap <nexmap@example.com>". Its domain must be one your mail service may send for |
NEXMAP_MAIL_MAX_PER_HOUR | 100 | Most emails the server sends in an hour, all kinds together. A safety net; the limits below normally apply first |
NEXMAP_SMTP_HOST | SMTP relay (transport smtp) | |
NEXMAP_SMTP_PORT | 587, or 465 with tls | |
NEXMAP_SMTP_TLS | starttls | starttls: the relay must offer STARTTLS, or nothing is sent. tls: TLS from the start (port 465). none: no encryption, only for a relay on the same host or network, and only without credentials |
NEXMAP_SMTP_USERNAME, NEXMAP_SMTP_PASSWORD | empty: no login | Credentials for the relay (AUTH PLAIN, over TLS only) |
NEXMAP_MAIL_API_KEY | Key or token of the mailer API: the Mailgun API key, the SendGrid API key, the MailPace server token, the Scaleway secret key or the Lettermint project token | |
NEXMAP_MAILGUN_DOMAIN | Sending domain at Mailgun (transport mailgun) | |
NEXMAP_MAILGUN_REGION | us | us or eu |
NEXMAP_SCALEWAY_PROJECT_ID | Project of the Transactional Email domain (transport scaleway) | |
NEXMAP_SCALEWAY_REGION | fr-par | Region of Scaleway Transactional Email |
The mail settings have no command line flags: they include secrets, and a process’s command line is
visible to other users of the machine. The server does not start if the mail configuration is incomplete
or invalid, or if a transport is set without NEXMAP_PUBLIC_URL. The links in emails are always built
from NEXMAP_PUBLIC_URL, never from the request, so nobody can make Nexmap send a reset link that points
to another site.
Services that are not in the list can be used through their SMTP relay, such as Amazon SES, Postmark or Brevo. Scaleway, MailPace and Lettermint can also be reached by SMTP, if you prefer that.
Examples:
# An SMTP relay with STARTTLS on port 587
NEXMAP_MAIL_TRANSPORT=smtp
NEXMAP_MAIL_FROM='Nexmap <nexmap@example.com>'
NEXMAP_SMTP_HOST=smtp.example.com
NEXMAP_SMTP_USERNAME=nexmap
NEXMAP_SMTP_PASSWORD='<password>'
# Scaleway Transactional Email through its API
NEXMAP_MAIL_TRANSPORT=scaleway
NEXMAP_MAIL_FROM=nexmap@example.com
NEXMAP_MAIL_API_KEY='<secret key>'
NEXMAP_SCALEWAY_PROJECT_ID='<project id>'
At startup the log says mail enabled with the transport, or mail disabled. Each email is logged as
mail sent or mail not sent with its purpose (invitation or password_reset), never with the
address or the link. See Security for what the emails contain and how they are
limited.
Logging
| Variable | Flag | Default | Meaning |
|---|---|---|---|
NEXMAP_LOGLEVEL | --log-level | info | debug, info, warn or error |
NEXMAP_LOGFILE | --log-file | empty: stdout | Write the log to this file (appended, mode 0600) instead of stdout |
Logs are JSON, one entry per line.
Development
| Variable | Flag | Default | Meaning |
|---|---|---|---|
NEXMAP_DEV_MODE | — | false | Enables the component catalogue at /dev/catalogue and fills a newly created database with demo products and users. Never enable it in production |
Value formats
- Boolean variables (
NEXMAP_COOKIE_SECURE,NEXMAP_DEV_MODE) are true fortrueor1; any other value means false. - An invalid duration or number (
NEXMAP_SESSION_TTL,NEXMAP_PBKDF2_ITERATIONS,NEXMAP_MAX_UPLOAD_BYTES) is ignored and the default is used, without a warning. Check the values carefully. - An unknown log level or database driver stops the server with an error.
- The mail settings are checked strictly: an invalid number, port or option stops the server with an error.
Health check
nexmapd healthcheck requests /api/v1/health from the server running with the same configuration and
exits with 0 if it answers. The container image uses it as its HEALTHCHECK; outside a container, the
endpoint can be polled directly.
Minimal production setup
NEXMAP_ENCRYPTION_KEY='[{"id":"2026-01","key":"<base64 key>"}]'
NEXMAP_ADMIN_USER=jdoe
NEXMAP_ADMIN_PASSWORD='<long random password>'
# for PostgreSQL instead of SQLite:
NEXMAP_DB_DRIVER=postgres
NEXMAP_DB_DSN='postgres://nexmap:<password>@db:5432/nexmap?sslmode=require'
Behind a TLS-terminating proxy, the remaining defaults are suitable for production.
Security
This page covers what an administrator has to set up and look after to run Nexmap safely. Every setting mentioned is described in the configuration reference.
TLS and the reverse proxy
nexmapd speaks plain HTTP. Run it behind a reverse proxy that terminates TLS (on Coolify, the built-in
Traefik does this), and do not expose its port to the network directly:
-
Leave
NEXMAP_COOKIE_SECUREattrue. Browsers then send the session cookie over HTTPS only, and the server sendsStrict-Transport-Security, so browsers that have visited once refuse plain HTTP for a year. -
The client address of sessions, failed logins and the limits below comes from the
X-Forwarded-Forheader, but only when the request arrives from a trusted proxy (NEXMAP_TRUSTED_PROXIES, by default loopback and private networks). A request from any other address is taken to come from that address, whatever its header says. From a trusted proxy the header is read from the end, skipping trusted proxies, so entries a client wrote itself do not count, whether the proxy replaces the header or appends to it. -
If machines other than the proxy can reach
nexmapdon a private network, setNEXMAP_TRUSTED_PROXIESto the proxy’s address. Otherwise they can claim any client address and evade the limits. -
The server refuses form posts and API calls that a browser sends on behalf of another site. Modern browsers state the origin of a request themselves; for older ones the server compares the
Originheader with theHostheader, so the proxy has to pass the originalHoston (Traefik and Caddy do; nginx needsproxy_set_header Host $host). SettingNEXMAP_PUBLIC_URLmakes that comparison independent of the proxy.
Response headers
Every response carries a Content-Security-Policy that only allows scripts, styles, images, fonts and
form targets from the server itself and forbids framing, plus X-Content-Type-Options,
X-Frame-Options, Referrer-Policy and Permissions-Policy. Images in Markdown that point to another
server are therefore not loaded. The policy still allows inline scripts, which the pages use, so it
limits what injected markup could do (load code from or send data to another server) rather than ruling
it out. If your proxy adds headers of its own,
make sure it does not replace these with weaker ones.
Request limits
Request bodies are capped at 1 MiB, attachment uploads at NEXMAP_MAX_UPLOAD_BYTES plus 1 MiB for the
form around the file, and test run uploads at 32 MiB; larger requests get 413. A request has to be
received within 5 minutes and answered within 10.
The encryption key
Personal data (email addresses and display names) is encrypted in the database with AES-256-GCM.
Usernames are treated as pseudonyms and are not encrypted. The key comes from NEXMAP_ENCRYPTION_KEY,
a JSON list of keys with an ID and a base64-encoded 32-byte key each:
[{"id": "2026-01", "key": "<32 random bytes, base64-encoded>"}]
To generate one:
printf '[{"id":"%s","key":"%s"}]\n' "$(date +%Y-%m)" "$(openssl rand -base64 32)"
Treat the key like a database password: keep it in your platform’s secret store, not in a repository.
Each encrypted value, including a user’s two-factor secret, is bound to the record and field it belongs to. Someone who can write to the database but does not have the key cannot move one user’s value into another user’s record: it no longer decrypts there. Values written by a version before this binding existed are still read and are bound the next time the record is saved.
Back up the key separately from the database. Without it, the personal data in a database backup cannot be decrypted, and Nexmap has no way to recover it. Conversely, a database backup stored together with its key protects nothing. See Backup and restore.
Rotating the key
The first key in the list encrypts new data; the others are kept only to decrypt data written with them. To switch to a new key, put it first and keep every older key in the list:
[{"id": "2027-01", "key": "<new key>"}, {"id": "2026-01", "key": "<old key>"}]
Never remove an old key: data encrypted with it would become unreadable.
Rotation is incomplete today, so rotate only when you have a reason to, such as a leaked key:
- Existing data is not re-encrypted. It stays encrypted with the old key until the record is next changed, so a leaked old key still exposes it.
- Email addresses also get a lookup hash derived from the first key, which is how the database prevents two accounts from sharing an address. After a rotation, that check only compares addresses hashed with the same key, so a duplicate of an address stored under the old key is not caught.
The first administrator
On the very first start, when the database has no users, nexmapd creates an administrator from
NEXMAP_ADMIN_USER and NEXMAP_ADMIN_PASSWORD. If the username is unset it is admin; if the password is
unset, a random one is generated and written to the log.
Set both variables for the first start. If you rely on the generated password, read it from the log, sign
in, change the password right away, and keep in mind that the log still contains it. The administrator’s
email address starts out as admin@localhost; change it on the profile page, or a password reset link
could not reach you.
Once users exist, both variables are ignored and can be removed.
Accounts and sign-in
-
Two-factor authentication: users can set up TOTP on their profile page. It can be required in two places:
- Instance-wide, under Two-Factor Authentication in the instance settings (
/admin/settings): for all users, for administrators, and for product owners, developers, testers or viewers. A role switch covers users who hold that role in at least one product. Users covered are sent to the setup page after signing in and cannot use Nexmap until 2FA is set up; API calls with their session get403 TOTP_SETUP_REQUIRED. A common setup is to require it for administrators and product owners and leave the rest to the products. - Per product, by its owners under Settings → General (Require two-factor authentication).
Members without 2FA, administrators included, cannot open that product until they set it up; their
other products keep working. The dashboard marks such products with a lock, and API calls to them
get
403 TOTP_SETUP_REQUIRED. Only a user with 2FA can turn the requirement on, and the team page shows owners which members still lack it.
Personal access tokens are not affected by either: a token is a separate credential. Users cannot turn 2FA off while any rule requires it of them. An administrator can reset a user’s 2FA from the user’s detail page, after which that user can sign in with the password alone until they set it up again (and is sent to setup, or kept out of 2FA products, as above). Should the server be unable to read the 2FA settings or a user’s product roles for a while (a database fault), it keeps acting on the values it read last, so enforcement does not lapse in the meantime.
- Instance-wide, under Two-Factor Authentication in the instance settings (
-
Usernames and email addresses are unique regardless of case:
Admincannot be registered next toadmin, and signing in accepts either spelling. -
Passwords are stored as salted PBKDF2-SHA256 hashes (
NEXMAP_PBKDF2_ITERATIONS). The instance settings can require uppercase and lowercase letters, digits and special characters in new passwords, and a minimum length from 8 to 128 characters. -
The password is asked for again before two things a session alone must not be enough for: deleting your own account, and creating an access token with the
accountoradminscope. -
The last administrator cannot be locked or deleted, by anyone including themselves, and a product always keeps at least one owner. There is no setting to make a user an administrator yet; the first administrator is the only one unless you change
is_adminin the database. -
Forgotten passwords: with mail, users can ask for a reset link on the sign-in page (see Email). Without mail, administrators set new passwords for users from the user’s detail page. Either way, the new password ends all of that user’s sessions, and a second factor stays on.
-
Sessions expire after
NEXMAP_SESSION_TTL(24 hours by default). The database stores only a hash of each session token, so a database backup contains nothing that works as a session cookie. Changing a password ends the user’s other sessions, keeping only the one the change was made from; an administrator setting a new password ends all of that user’s sessions. Personal access tokens stay valid either way; revoke them separately if they may have leaked. Locking an account blocks its sessions and tokens at once. -
Failed logins are limited per client address: after 5 wrong passwords for the same username, or 20 for any usernames, within 15 minutes, that address has to wait until the 15 minutes are over. The web form says so and the API answers
429 TOO_MANY_ATTEMPTSwith aRetry-Afterheader. A correct password clears the count for that username. There is no limit per account across addresses, so nobody can lock another user out by guessing their password. The counters live in memory and reset when the server restarts. -
Wrong two-factor codes are limited per user: after 10 wrong codes (from the authenticator or recovery codes) within an hour, no code is accepted for that user until the hour is over, at sign-in and when changing the two-factor settings alike. This takes the user’s password or session, so it cannot be used to lock out a stranger; a user who is blocked this way should change their password. The server logs
second factor blocked after repeated wrong codeswith the user ID.
Records of administrative actions
Changes to a product’s team (adding and removing members, changing roles, transferring ownership) are
recorded in the product’s audit trail. Administrator actions (approving, locking, unlocking and deleting
users, setting passwords, resetting 2FA, changing instance settings) are written to the server log as
administrator action with the administrator’s and the affected user’s ID and the client address. Keep
the log if you need to trace them later.
New accounts
People get an account in one of two ways:
- Invitation: a product owner invites someone to their product with a role (on the product’s Team
page), or an administrator invites someone to the installation (
/admin/invitations). With mail, the inviter chooses whether Nexmap emails the link to the invited address or shows it to the inviter to pass on; without mail, Nexmap always shows it (built fromNEXMAP_PUBLIC_URLif set). Whoever holds the link can create the account, so treat it as a secret: it works once, expires after 7 days, and is never written to the log. An emailed link is not shown to the inviter, so the new account has proven that its owner reads that mailbox. Accounts created from an invitation skip moderation. - Self-registration, if self-registration is enabled in the instance settings: anyone who can reach
the server can create an account on
/register. With moderation enabled, such accounts wait for an administrator’s approval under/admin/usersbefore they can sign in. A self-registered account belongs to no product until a product owner adds it.
Creating accounts is limited to 20 attempts per client address and hour, for self-registration and
invitations together; after that the form and the API (429 TOO_MANY_ATTEMPTS) ask to wait.
Only product owners (and administrators) can invite to a product, and only administrators can invite to the installation without a product.
Nexmap sends email only when configured, and then only invitations and password reset links. It sends nothing else (no notifications), and no part of Nexmap lets anyone choose what an email says. The safeguards:
- Fixed text. Both emails are fixed text with the address of the installation, the link and its expiry; an invitation also names the role. Nothing a user typed (product names, display names) is put into an email, so nobody can use Nexmap to carry their own message to an address of their choosing.
- One recipient. Every email goes to exactly one plain address. Lists, display names and line breaks are refused, so the recipient and headers cannot be manipulated.
- Invitation emails need a signed-in product owner or administrator (or their access token with
settings:writeoradmin). Each inviter can send 20 an hour, and each address receives at most 3 a day, from all inviters together. Beyond that, the form and the API (429 TOO_MANY_ATTEMPTS) ask to wait; invitation links can still be created. - Password reset links go only to the address stored on an existing, active account, never to an
address typed into the form. The form at
/forgot-passwordanswers the same for every input, and sends in the background, so neither the answer nor its timing tells whether an account exists. A client address may ask 5 times an hour, and an account receives at most 3 reset emails an hour, whoever asks. There is no API endpoint for it. - Reset links work once and for 60 minutes; asking again replaces the previous link. The database stores only a hash of the token. Using the link sets the new password, ends all of the user’s sessions, and leaves two-factor authentication on: the user signs in anew with the second factor. Locked accounts, and accounts awaiting approval or deletion, cannot use a reset link.
- A cap for the installation,
NEXMAP_MAIL_MAX_PER_HOUR(100 by default), limits all emails together, whatever the other limits let through. - Links point to
NEXMAP_PUBLIC_URLonly, which is required with mail. A reset link built from the request’sHostheader could be made to point to someone else’s server. - Tracking is off for the mailer APIs that have a switch for it (Mailgun, SendGrid, Lettermint): tracking would route every link, token included, through the provider’s redirector.
- SMTP is encrypted: with
starttls, Nexmap refuses to send through a relay that does not offer STARTTLS, and it sends credentials over TLS only.
The limits are kept in memory and reset when the server restarts, like the login limits. Reset links waiting to be sent are kept in memory too; one still queued when the server stops is lost, and the user asks again.
Access tokens
Personal access tokens are what the CLI, CI jobs and AI agents use. They are accepted only by the REST API, never by the web UI’s pages. Users can narrow a token to some products, to read or write access per area, and to a lower role (see Restricted tokens); a token never grants more than its user holds at the time of the request.
Two rights are never granted by default: managing the user’s own account and tokens (account), and
administration (admin). Granting either from the web UI or a session needs the user’s password. An administrator’s token without the admin scope has no administrator rights,
so a leaked CI token cannot reach the instance settings. A token can create further tokens only with the
account scope, and only narrower than itself.
Under Instance Settings, Maximum Token Lifetime forces every new token to expire within the given
number of days (0, the default, allows tokens without expiry). The token list shows when each token was
last used; revoke those that are no longer needed under API Tokens (/user/tokens).
Attachments
By default, every file type can be uploaded. To restrict this, set NEXMAP_ALLOWED_UPLOAD_TYPES; the
server then also checks that the content matches the extension. Attachments are always served as
downloads and never rendered by the browser, but Nexmap does not scan them for malware.
Development mode
NEXMAP_DEV_MODE fills a new database with demo users whose password is publicly known (it is in the
source code and the log). Never enable it on a server that others can reach.
Backup and restore
Nexmap does not make backups itself. A complete backup has three parts:
| Part | Where | Notes |
|---|---|---|
| Database | NEXMAP_DB_DSN: the SQLite file (container: /data/nexmap.db) or the PostgreSQL database | Everything except attachment contents |
| Attachments | NEXMAP_UPLOAD_DIR (container: /data/uploads) | One directory per product, one file per attachment |
| Encryption key | NEXMAP_ENCRYPTION_KEY | Needed to read personal data in the database |
Keep the key apart from the database backups. Without the key, the email addresses and display names in a backup cannot be decrypted, and Nexmap has no way to recover them. A backup stored together with its key, on the other hand, protects nothing. Store the key once, in a password manager or your platform’s secret store, and again whenever you rotate it: a backup needs every key that was in the list when it was taken.
Back up the database first, then the attachments. An attachment uploaded in between is then just an unused file in the backup. The other order could leave a database that references files the backup lacks.
SQLite
The database runs in WAL mode, so the file on disk alone is not a consistent copy while the server
runs: recent changes may still sit in nexmap.db-wal. Do not copy the files with cp while the server
runs. Use SQLite’s online backup instead, which produces a consistent single file without stopping the
server:
sqlite3 /path/to/nexmap.db ".backup '/backups/nexmap-$(date +%F).db'"
The container image has no sqlite3. Run it on the host, against the file in the volume mounted at
/data (for Docker volumes, typically /var/lib/docker/volumes/<volume>/_data/nexmap.db). The host
and the container must share the same file system; this does not work over a network share.
Alternatively, stop the server and copy nexmap.db together with any nexmap.db-wal and
nexmap.db-shm next to it.
Then the attachments:
tar -czf /backups/nexmap-uploads-$(date +%F).tar.gz -C /path/to/data uploads
PostgreSQL
Use pg_dump, which takes a consistent snapshot while the server runs:
pg_dump --format=custom --file=/backups/nexmap-$(date +%F).dump "$NEXMAP_DB_DSN"
Back up the attachment directory as for SQLite.
Restoring
- Stop
nexmapd. - Put the database back:
- SQLite: replace
nexmap.dbwith the backup file and delete anynexmap.db-walandnexmap.db-shmleft next to it, as they belong to the replaced database. - PostgreSQL:
pg_restore --clean --if-exists --no-owner --dbname="$NEXMAP_DB_DSN" nexmap.dumpdrops the existing tables and restores those of the backup.
- SQLite: replace
- Replace the attachment directory with the one from the backup.
- Make sure
NEXMAP_ENCRYPTION_KEYcontains the key (or keys) the backup was written with. - Start
nexmapd.
A restored database is as it was at the time of the backup, sessions included: everyone signed in then may still be signed in, and later password changes are undone. After restoring because of a security incident, have users change their passwords, and revoke API tokens that may have leaked.
A backup from an older Nexmap version can be restored into a newer one: the missing migrations run at
startup. The reverse is refused: nexmapd does not start on a database migrated by a newer version.
Upgrades
Migrations run automatically when a new version starts and cannot be undone. Back up before every
upgrade. An instance that deploys the stable branch automatically upgrades as soon as a release is
made, so schedule its backups accordingly, or deploy a release tag and upgrade by hand. To go back to
the previous version, restore that backup and start the previous version on it.
Test the restore
Before relying on the backups, restore one into a separate instance (another directory or container, with the same encryption key) and check that you can sign in, open a product and download an attachment. Repeat this after changing how backups are taken.
Setting up the CLI
nexmap is the command line client for a Nexmap server. It talks to the server only through the
REST API, so it needs nothing but the server’s URL and a personal access token.
Installing
nexmap is a single binary without runtime dependencies, published for Linux, macOS and Windows on
amd64 and arm64 at nexmap.net.
On Linux and macOS:
curl -fsSL https://nexmap.net/install.sh | sh
On Windows, in PowerShell:
irm https://nexmap.net/install.ps1 | iex
The scripts download the latest release for your platform, verify it against the release’s checksums and install it:
install.shinto~/.local/bin, or/usr/local/binwhen run as root. It tells you when that directory is not on yourPATH.install.ps1into%LOCALAPPDATA%\Programs\nexmap, which it adds to your userPATH.
They read these environment variables:
| Variable | Meaning |
|---|---|
NEXMAP_VERSION | Version to install, e.g. v1.0.0; default: the latest release |
NEXMAP_INSTALL_DIR | Directory to install into |
To pin a version on Linux or macOS: curl -fsSL https://nexmap.net/install.sh | NEXMAP_VERSION=v1.0.0 sh.
In PowerShell, set $env:NEXMAP_VERSION = 'v1.0.0' first.
To install by hand, download the archive for your platform from
nexmap.net/releases, check it against the release’s SHA256SUMS
(on Linux: sha256sum -c SHA256SUMS --ignore-missing), and put the binary on your PATH.
Updating
nexmap update replaces the running binary with the latest release; nexmap update v1.0.0 installs a
given version, which may be an older one. nexmap update --check only reports whether a newer release
exists. The download is verified against the release’s checksums before anything is replaced.
update refuses to install a release that needs a newer API than the configured server offers (see
nexmap version), since that client could not talk to it; --force installs it anyway, and also
reinstalls the current version. If the binary sits in a directory you cannot write to, run the update
with the rights to write there, e.g. sudo nexmap update.
Once a day, nexmap looks for a newer release in the background and prints a one-line notice on stderr
if there is one. The check never delays a command by more than two seconds and stays silent on errors.
It is off where an extra line could get in the way: when stderr is not a terminal (scripts, pipes), when
CI is set, for nexmap mcp, and for development builds. NEXMAP_NO_UPDATE_CHECK=1 turns it off
altogether.
Logging in
-
In the web UI, open API Tokens in the user menu (
/user/tokens) and create a token. -
Store it:
nexmap login --server https://nexmap.example.comOn a terminal,
loginprompts for the token without echoing it. Otherwise it reads the first line of stdin, which suits scripts:printf '%s\n' "$TOKEN" | nexmap login --server …. There is deliberately no flag for the token itself, as it would end up in shell history and process listings.
login checks the token with the server and stores it per server in the user configuration file
(config.json in the user configuration directory, see below), readable only by you. nexmap logout
removes it again; nexmap whoami shows the user, server and product in effect and where each value
came from.
When you create a token, narrow it to what it is for: pick a preset, or set the scopes, a role cap and
the products yourself. CI ingestion suits nexmap utest, Agent, spec author an AI agent that maintains the
specification, and Agent, read-only suits an AI agent that only
reads. A token never grants more than its user holds; nexmap whoami shows its restriction. See
Restricted tokens for what each scope covers.
Connecting a repository
In the repository, run:
nexmap init
This writes .nexmap.json at the repository root (or into --dir) with the server and product:
{
"server": "https://nexmap.example.com",
"product": "<product id>"
}
The product comes from --product if given; if you are a member of only one product, that one is used;
otherwise init lists the products and asks. Without a terminal it fails and lists the choices instead,
so pass --product in scripts. --force overwrites an existing file, and --agents additionally sets
up AI agents (see AI agents).
Commit .nexmap.json: it never contains a token, and every command run anywhere in the repository finds
it by walking up from the current directory.
Configuration
Each setting is taken from the first source that has it, in this order:
- Command line flags
- Environment variables
.nexmap.jsonin the current directory or the nearest parent- The user configuration file
| Setting | Flag | Environment | .nexmap.json | User configuration |
|---|---|---|---|---|
| Server URL | --server | NEXMAP_SERVER | server | — |
| Product ID | --product | NEXMAP_PRODUCT | product | — |
| Output format | -o, --output | NEXMAP_OUTPUT | — | output |
| Token | — | NEXMAP_TOKEN | — | stored per server by login |
| Plain HTTP | --allow-http | NEXMAP_ALLOW_HTTP | — | — |
NEXMAP_TOKEN takes precedence over a stored token; login warns when it is set. Unlike a stored
token, which is kept per server, it is sent to whichever server is configured. The CLI warns when that
server comes from .nexmap.json; set NEXMAP_SERVER next to the token to pin it. Boolean variables
accept 1, true, yes or on.
The user configuration directory is nexmap inside the operating system’s configuration directory:
$XDG_CONFIG_HOME/nexmap (usually ~/.config/nexmap) on Linux, ~/Library/Application Support/nexmap
on macOS, %AppData%\nexmap on Windows. Set NEXMAP_CONFIG_DIR to use another directory.
Two more variables concern updates (see Updating): NEXMAP_NO_UPDATE_CHECK turns off the
daily check for a newer release, and NEXMAP_RELEASES_URL points update and the check at another
location than https://nexmap.net/releases, e.g. a mirror.
Plain HTTP
The CLI refuses http:// servers unless they run on the local machine (localhost, *.localhost or a
loopback address), so a token never crosses the network unencrypted. --allow-http (or
NEXMAP_ALLOW_HTTP=1) lifts this deliberately, e.g. on a trusted internal network.
Commands
| Command | Purpose |
|---|---|
login, logout, whoami | Manage the stored token, show what is in effect |
init | Write .nexmap.json; with --agents, set up AI agents |
version | Client version and, if a server is configured, the server version |
update | Install the latest or a given release, see Updating |
context | Product briefing: structure, constraints, work in progress, defects, testing status |
req, task | Requirements and tasks: list, show, status, create |
constraint | list, show, create, update (e.g. --compliance met) |
doc | Documents: list, show, create, update, delete, link/unlink, revisions, diff, restore, get |
component | Product structure: list, create, update, delete |
capability | list, show, create, update, delete, and link/unlink requirements and constraints |
defect | list, show, status, create |
test | Test cases: list, show, sync, and link/unlink them to a requirement, capability or component |
campaign | status of the active testing campaign |
comment | add a comment to any commentable item |
utest | Record test results from CI, see CI integration |
export | The product’s specification as Markdown, see below |
api | Raw REST call, see Scripting |
agent-guide, mcp | For AI agents, see AI agents |
nexmap help <command> (e.g. nexmap help req show) lists a command’s arguments and flags.
Requirements, tasks, constraints, documents and defects are referenced by their identifier (FR-12, MT-3,
AC-42, DOC-3, DF-17), test cases by their short ID (the first 8 characters of the ID, shown in lists) or
their test identifier, and capabilities by name.
Changing the structure
Product owners can build the product structure from the command line, which helps when an AI agent helps develop the product:
nexmap component create --name "Sync engine" --body "Keeps devices in step."
nexmap capability create --name "Offline queue" --component "Sync engine"
nexmap capability link Offline queue FR-12 AC-3
nexmap constraint update AC-3 --compliance violated
nexmap test link TestQueue_Replay --req FR-12 --capability "Offline queue"
Names with spaces need no quotes where they are the command’s only argument. For capability link,
everything before the first identifier names the capability; a capability whose name ends in something
like Step-1 has to be given by ID there. update commands change only the options given, and an
empty value clears a description or, for capability update --component "", moves a capability to
product level. component delete and capability delete need --yes.
Documents
Documents hold the protocols, schemas and other texts that define the product. Nexmap is where they are written; a repository gets copies through the export below. A body can come from a file and goes back out unchanged:
nexmap doc create --title "Sighting schema" --format json --state current --body-file schema.json
nexmap doc update DOC-3 --body-file schema.json
nexmap doc diff DOC-3
nexmap doc link DOC-3 FR-12 SC-2 TestSchema_Validates
nexmap doc get DOC-3 --file api/schema.json
Every change to a document’s title, content or format is a new revision; doc revisions lists them,
doc diff compares two (--from, --to), doc restore DOC-3 2 makes an earlier content current again
and doc get --revision 2 prints it. Setting a body from a file is an ordinary update: Nexmap does not
watch the file. Only product owners change documents; doc delete needs --yes.
Exporting the specification
nexmap export writes the product’s structure (components and capabilities), constraints and
requirements as one Markdown document: grouped by category, each item with its identifier, its
description, definition of done and linked capabilities, and refinements nested below the item they
refine. --tasks adds the tasks, --file docs/spec.md writes the document to a file instead of stdout.
The specification also lists the current documents, and with --drafts the drafts. --documents DIR
writes each listed document’s content to its own file there, byte for byte, named by its identifier and
format (DOC-3.json), so tools in the repository, such as code generators and schema validators, can use
it directly:
nexmap export --file docs/spec.md --documents docs/definitions
The order is fixed (categories as configured, items by number, structure by name) and the document
contains no date, status or priority, so exporting an unchanged product gives the same file, and a
committed export shows real changes in a diff. With -o json and --file, the output names the file and
the document’s SHA-256. AI agents get the same document from the MCP tool export_specification. Anyone who can read the product can export it; a
restricted token needs read on both structure and backlog, and documents to include the
documents.
There is no import. To turn an existing document into requirements, let an AI agent read it and create
the items with nexmap req create or the REST API. Since the export carries identifiers, the agent can
change existing items by identifier instead of creating duplicates.
Scripting with the CLI
The output formats and exit codes below are a stable contract for scripts and AI agents.
Output formats
| Format | Use |
|---|---|
-o table | Plain tables and records for people. The default |
-o md | Compact markdown with the same headings every time; best for pasting into an AI agent’s context |
-o json | Machine-readable, with stable field names; use it for anything that parses the output |
The default is table whether or not stdout is a terminal, so a command’s output never changes with the
environment it runs in: scripts must ask for -o json explicitly. To change your personal default, set
NEXMAP_OUTPUT or output in the user configuration file (see Setup).
Errors and exit codes
| Exit code | Meaning |
|---|---|
0 | Success |
1 | Usage or configuration error (bad arguments, no server configured, ambiguous reference) |
2 | API error: server unreachable, authentication failed, or not permitted for your role |
3 | Not found: the item does not exist or is not visible to you |
4 | Tests failed (only utest --exit-code) |
Errors go to stderr as a single line, nexmap: <message>, followed by a hint for authentication and
role errors. With -o json, stderr carries {"error": {"code": "…", "message": "…"}} instead.
A role refusal (exit 2) is final: retrying will not help; the user needs a different role in the product.
Long texts
Commands that take a description or comment body accept it from a file or stdin (--body-file -), which
avoids shell quoting problems with markdown:
nexmap comment add FR-12 --body-file - <<'EOF'
Decided to keep the old endpoint until **v2**.
EOF
Raw API calls
For anything without a dedicated command, nexmap api sends an authenticated request to the
REST API and prints the response:
nexmap api /products/{product}/requirements -o json
nexmap api POST /products/{product}/requirements --data @new.json
generate-body | nexmap api POST /products/{product}/requirements --data @-
The method defaults to GET; {product} in the path is replaced with the configured product; --data
takes JSON text, @file or @- for stdin.
Retries
Read requests (GET, HEAD, OPTIONS) are retried automatically after network errors and 429,
502, 503 and 504 responses. Requests that change data are sent once, so a failed write is never
applied twice behind your back.
CI Integration
nexmap utest records automated test results in the product’s active testing campaign (FR-018, FR-409). It reads a test report from stdin or from files, maps each test to an automatic test case by its test identifier, and uploads the run in one request.
go test -json ./... | nexmap utest --env linux-amd64
go test -v ./... | nexmap utest --identifier-style name # format auto-detected
nexmap utest --format junit reports/*.xml
What happens to a run
| Situation | Effect |
|---|---|
| Test passes / is skipped | Its campaign result is set to passed / skipped; a defect on the result is cleared |
| Test fails | Result set to failed. An open defect already on the result is reused; otherwise a defect Automated test failed: <identifier> is created with the run metadata and the test’s output (last 64 KiB), linked to the test case |
| Status unchanged since the last run | Nothing is written, so repeated CI runs do not flood the result history |
Subtests (TestX/case) without their own test case | Rolled up into TestX: any failure fails it, the failing subtests’ output goes into the defect |
| Test case runs per environment | Needs --env NAME matching a runtime environment of the product; other environments’ results are untouched |
| No automatic test case has the identifier, or the test case is not in the campaign | Not recorded, no defect; failures are still listed in the build log |
| No active campaign | The upload is accepted, nothing is written (no results, events or defects), and the CLI prints no active campaign — results not recorded |
The FR-504 loop works with automation: closing the defect moves the result to re-test, and the next green run marks it passed. A later failure files a new defect, since the old one is closed.
Each status change is recorded in the result history with the token’s user, the report format (go-test, junit), and the revision and branch.
Setting up
- Create a token for CI under API Tokens in the user menu (
/user/tokens) with the CI ingestion preset (tests:writeonly), restricted to the product, and with a role cap of tester. The job then cannot touch anything but test results, even with a token from a product owner. A dedicated CI user with the tester role works as well. - Register the tests as automatic test cases with a test identifier.
nexmap test synccreates the missing ones from a report (developer role):go test -json ./... | nexmap test sync --dry-run # review first go test -json ./... | nexmap test sync--componentor--capabilityfiles the new test cases in the product structure,--pyramid-level unitsets their level and--req FR-12links them to the requirement they verify; all options are checked before anything is created. Subtests (TestX/case) get no test case of their own when their parent is in the report, since results roll up into the parent. Use the same--identifier-stylehere as fornexmap utest. New test cases join campaigns created afterwards; an active campaign keeps the scope it was created with. - Check the mapping before wiring CI:
go test -json ./... | nexmap utest --dry-runshows how each test maps to a test case and what would change. - Install the CLI in the job with the install script (see Setup), pinned to a version so a new release never changes a build unannounced:
curl -fsSL https://nexmap.net/install.sh | NEXMAP_VERSION=v1.0.0 sh. As root (most CI containers) it installs into/usr/local/bin, otherwise into~/.local/bin, which may need adding to the job’sPATH. The update notice stays off in CI. - Configure the job with
NEXMAP_SERVER,NEXMAP_PRODUCTand the secretNEXMAP_TOKEN. The product may come from a committed.nexmap.jsoninstead (see Setup), but set the server in the job: the token goes to whatever server is configured, and a branch that changes the file must not be able to send it elsewhere. The CLI prints a warning when it sendsNEXMAP_TOKENto a server taken from.nexmap.json.
Options
| Flag | Meaning |
|---|---|
--format auto|go-json|go-text|junit | Report format; auto sniffs the input (JSON events → go-json, XML → junit, else go test -v text) |
--identifier-style package|name | package (default): pkg/path.TestName for Go, classname.name for JUnit; name: the bare test name. Match whatever your test cases use |
--env NAME | Runtime environment of this run (case-insensitive); unknown names are rejected |
--campaign NAME | Record in this campaign instead of the active one (must be active) |
--revision, --branch | Default from GITHUB_SHA/GITHUB_REF_NAME, CI_COMMIT_SHA/CI_COMMIT_REF_NAME, … or git rev-parse |
--dry-run | Evaluate and print the mapping; write nothing |
--exit-code | Exit 4 if any test (or a package build) failed |
--quiet | Do not copy stdin to stdout |
When reading stdin, utest copies its input to stdout so the test log stays in the build output, then prints a summary. With -o json it prints only the JSON report.
Exit status. In go test -json ./... | nexmap utest, the shell reports the exit status of nexmap, not of go test. Either let nexmap fail the job with --exit-code, or use set -o pipefail (bash) so a failing go test still fails it. Without either, the job passes even when tests fail.
GitHub Actions
name: test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Install nexmap
run: |
curl -fsSL https://nexmap.net/install.sh | NEXMAP_VERSION=v1.0.0 sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Test
env:
NEXMAP_SERVER: https://nexmap.example.com
NEXMAP_PRODUCT: ${{ vars.NEXMAP_PRODUCT }}
NEXMAP_TOKEN: ${{ secrets.NEXMAP_TOKEN }}
run: go test -json ./... | nexmap utest --env linux-amd64 --exit-code
For a build matrix, pass the matrix value: --env ${{ matrix.os }}, with one runtime environment per OS in the product.
GitLab CI
test:
image: golang:1.26
variables:
NEXMAP_SERVER: https://nexmap.example.com
NEXMAP_PRODUCT: "<product id>"
# NEXMAP_TOKEN: set as a masked CI/CD variable
script:
- curl -fsSL https://nexmap.net/install.sh | NEXMAP_VERSION=v1.0.0 sh
- go test -json ./... | nexmap utest --env linux-amd64 --exit-code
Forgejo / Gitea Actions
Forgejo Actions sets the same GITHUB_* variables, so revision and branch are picked up automatically.
on: [push]
jobs:
test:
runs-on: docker
container:
image: golang:1.26
steps:
- uses: https://code.forgejo.org/actions/checkout@v4
- run: curl -fsSL https://nexmap.net/install.sh | NEXMAP_VERSION=v1.0.0 sh
- env:
NEXMAP_SERVER: https://nexmap.example.com
NEXMAP_PRODUCT: ${{ vars.NEXMAP_PRODUCT }}
NEXMAP_TOKEN: ${{ secrets.NEXMAP_TOKEN }}
run: go test -json ./... | nexmap utest --env linux-amd64 --exit-code
Other ecosystems
Anything that writes JUnit XML works: pytest (--junitxml=report.xml), jest (jest-junit), Maven Surefire (target/surefire-reports/*.xml), gotestsum (--junitfile).
pytest --junitxml=report.xml; status=$?
nexmap utest --format junit report.xml
exit $status
With the default package style, pytest identifiers look like tests.test_calc.test_add and Surefire ones like com.example.CalcTest.testAdd; jest-junit’s default (classname equal to name) yields the test title.
The upload endpoint behind nexmap utest is described in the REST API reference.
AI agents
AI coding agents working in a repository can read the product definition, backlog and QA state from Nexmap, and, within their user’s role, update it. They use either the CLI or, for agents that speak the Model Context Protocol (Claude Code and others), the same operations as MCP tools.
Setting up a repository
After connecting the repository and logging in:
nexmap init --agents
This does two things on top of nexmap init (an existing .nexmap.json is kept):
-
registers the MCP server in the repository’s
.mcp.json, keeping any other servers in it:{ "mcpServers": { "nexmap": {"command": "nexmap", "args": ["mcp", "--read-only"]} } }An existing
nexmapentry is left alone unless you pass--force. -
appends the agent guide (
nexmap agent-guide) toCLAUDE.mdif that exists, otherwise toAGENTS.md, once.
The MCP server uses the same configuration and token as the CLI, so nothing else is needed. If the
configuration or token is missing, the server still starts and every tool call returns the reason
(e.g. “run nexmap login”), which the agent can pass on to you.
What agents can do
nexmap agent-guide prints the guide agents are given: the commands and tools, how items are
referenced, and working rules such as treating constraints as binding. Run it to see exactly what an
agent is told. In short:
- Read:
nexmap context -o md(or theget_contexttool) for a product briefing, then requirements, tasks, constraints, structure, defects, test cases and the campaign status as needed.nexmap export(or theexport_specificationtool) gives the whole specification in one Markdown document, which an agent can keep a spec file in the repository in sync with. The tool also returns the document’s SHA-256, so the agent can tell whether the file is current without comparing it. The tool is available on a read-only server too. - Work from documents: protocols, schemas and other definitions live in Nexmap as documents
(
nexmap doc show DOC-3, theget_documenttool). A requirement, constraint or test case names the documents linked to it, so an agent finds the definition it has to implement or check code against;doc diff(thedocument_difftool) shows what changed in a definition since the code was written. - Turn documents into requirements: Nexmap has no import. An agent can read a document you give it
and create the requirements with the CLI, and, using the identifiers from
nexmap export, update existing ones instead of duplicating them. - Write: set the status of requirements, tasks and defects, add comments, report defects, create tasks, create and update constraints (including whether they are met), change the product structure, write documents and restore their earlier revisions, link requirements, constraints and test cases to capabilities and documents, and test cases to requirements, each only as far as the user’s role allows (see below). The MCP server offers none of the CLI’s deletions.
Allowing writes
init --agents registers the MCP server with --read-only, which leaves out all tools that change
data. To let agents write through MCP, remove --read-only from the entry in .mcp.json. The CLI’s
write commands are always available to an agent that can run shell commands, so --read-only is a
convenience, not a guarantee: give an agent that should only read a token created with the Agent,
read-only preset, and the server refuses its writes. The Agent, spec author preset suits an agent that
maintains the specification: it writes the structure, the backlog, documents and comments, reads defects
and tests, and cannot change the product’s settings.
Writes are checked by the server against the token user’s role in the product, exactly as in the web UI:
| Action | Role needed |
|---|---|
| Set the status of a requirement, task or defect | developer, and only on items a product owner has activated (kanban: by moving them into an active column; scrum: by planning them into a sprint) |
| Comment | developer; tester on defects and test cases |
| Report a defect | tester |
| Link a test case to a requirement, capability or component; create test cases from a test run | developer |
| Create a task | product owner |
| Create or update a constraint, component or capability; link capabilities | product owner |
| Create, update, link or restore a document | product owner |
Each role includes the ones below it (product owner, developer, tester, viewer), and administrators may do everything.
A token’s restriction applies on top: the server also refuses writes outside the token’s scopes, above its role cap, or in products it is not restricted to. Give an agent its own token, narrowed to what it should do, rather than the token you use yourself.
REST API
The REST API under /api/v1 is the interface for the nexmap CLI and third-party systems. The web UI uses
its own endpoints and does not go through it.
Authentication
Requests authenticate with a personal access token, created in the web UI under /user/tokens:
Authorization: Bearer <token>
Tokens are accepted only under /api/; the web UI’s pages ignore them.
Restricted tokens
A token can be narrowed when it is created, and never grants more than its user holds at the time of the request:
-
Scopes. Each scope is granted as
readorwrite;writeincludesread. Without scopes, a token getswriteon every product scope, but neveraccountoradmin.Scope Covers structurecomponents, capabilities, milestones, sprints backlogrequirements, tasks, constraints documentsdocuments and their revisions defectsdefects teststest cases, campaigns and their results, test runs discussioncomments and attachments settingsproduct settings, members, categories, statuses, pyramid levels, runtime environments account/user/me/*: your profile, password, two-factor setup and tokensadmin/admin/*; without it, an administrator’s token has no administrator rightsA request needs
readon every resource named in its path, andwriteon the first one if it changes data:POST /products/{p}/capabilities/{id}/requirementsneedsstructure:writeandbacklog:read. Side effects belong to the request’s own scope, so uploading a test run needs onlytests:writealthough it files defects. Linking a document to a requirement or constraint needsdocuments:writeandbacklog:read, to a test casedocuments:writeandtests:read. Comments and attachments also needreadon the item they belong to. The audit trail needsreadon every product scope.GET /user/meis open to every token. A missing scope is answered with403and the codeTOKEN_SCOPE. -
Role cap. The token acts with the lower of its user’s role and the cap (
developer,testerorviewer). -
Products. The token reaches only the listed products, and
GET /productslists only those. Such a token cannot create products.
GET /api/v1/user/me shows the restriction of the token in use as token.
Tokens
POST /api/v1/user/me/tokens (account:write) creates a token:
{
"name": "CI",
"expires_at": "2027-01-31T23:59:59Z",
"scopes": ["tests:write"],
"role_cap": "tester",
"products": ["7d1c…"]
}
All fields but name are optional; leave out products for a token that reaches every product of its
user. A request made with a web session has to add "password", the user’s current password, to grant
the account or admin scope; so does POST /api/v1/user/me/delete. The response contains the token once, as raw_token. A request made with a token can only create
tokens that are narrower than its own. If the instance limits the token lifetime, expires_at is required
and must lie within the limit. GET /api/v1/user/me/tokens lists the tokens with their restrictions, and
DELETE /api/v1/user/me/tokens/{id} revokes one.
Identifiers
Wherever a path names a requirement, task, constraint, document or defect by ID, its identifier works as well:
GET /api/v1/products/{p}/defects/DF-17 is the same as using the defect’s ID. Identifiers are not case
sensitive. Responses carry both: id and identifier. Defect identifiers are also accepted for the
defect_id of a campaign result and for the owner_id of comments and attachments on a defect. The
defect prefix is the product’s defect_prefix, the document prefix (DOC-3) its document_prefix;
PATCH /api/v1/products/{p} changes either. The two must differ from each other and from every category
prefix of the product.
Responses
Successful responses wrap their payload in data; errors carry a machine-readable code and a
human-readable message:
{"data": { … }}
{"error": {"code": "…", "message": "…"}}
Concurrent edits
A PATCH of a requirement, task, defect, constraint, component, capability, milestone, sprint,
document, test case, campaign or product accepts if_updated_at: the updated_at of the version the
change is based on. If the item has been changed since, nothing is written and the answer is
409 CONFLICT; read it again and reapply the change. Without if_updated_at the change applies to
whatever is current.
Endpoints
Team, workflow and campaign scope
POST /api/v1/products/{p}/invitations(owner;settings:write) invites someone who has no account yet, with{"email": …, "role": …}, and answers with the invitation’surl. With"delivery": "email"(only if the server sends mail), Nexmap emails the link to that address instead, and the answer carries neitherurlnorraw_token. Invitation emails are limited per inviter and per address (429 TOO_MANY_ATTEMPTS); if the email cannot be sent, the answer is502 MAIL_NOT_SENTand no invitation is left behind."delivery": "link"is the default.POST /api/v1/products/{p}/statuses/split(owner;settings:write) gives defects a status set of their own. It cannot be undone.POST /api/v1/products/{p}/campaigns/{id}/test-caseswith{"test_case_ids": […]}andDELETE /api/v1/products/{p}/campaigns/{id}/test-cases/{testCaseId}(owner;tests:write) change the scope of a draft campaign; an active campaign’s scope is locked.- A campaign keeps its own copy of each test case in its scope. A draft campaign follows edits to the
test case; from activation on, the copy is frozen. Campaign results carry the copy’s
test_titleand theenvironment_namethey ran in.- Deleting a test case or an environment:
- Draft campaigns drop it.
- Active campaigns keep the results that have a verdict and drop those still
pendingorretest. - Closed campaigns keep everything.
- Kept results then have
test_case_idnull, orenvironment_idempty, but keep their title, environment name and history.
- Deleting a test case or an environment:
DELETE /api/v1/products/{p}/requirements/{id}deletes a requirement or task. With?children=keep, the default, the items below it move up one level, to its parent or to the top level; with?children=delete, they are deleted too.
Documents
Documents are the protocols, schemas and other texts that define the product. Nexmap holds them; a repository gets copies through the export.
GET /api/v1/products/{p}/documentslists them, without bodies unless?body=true; filter withstate,format,component_id,capability_id,milestone_idandq(title or identifier).POST /api/v1/products/{p}/documents(owner) creates one fromtitle,body,format(markdown, the default, ortext,json,yaml,xml,proto,sql,graphql),state(draft, the default,currentorsuperseded), and optionallycomponent_idorcapability_id(not both),milestone_idand, for a superseded document,superseded_by_id. The body is stored byte for byte, up to 512 KiB of valid UTF-8 without NUL characters.GET,PATCHandDELETE /api/v1/products/{p}/documents/{id}. APATCHthat changes the title, body or format creates a new revision;revisionis the number of the latest one.GET …/documents/{id}/raw[?revision=n]returns the body itself, always as atext/plainattachment named likeDOC-3.json, so a browser never renders it.GET …/documents/{id}/revisionslists the revisions,GET …/revisions/{n}returns one with its body,GET …/diff?from=&to=compares two (by default the latest with the one before) asunifiedtext and aslines, andPOST …/restorewith{"revision": n}(owner) makes an earlier content current again as a new revision.GET,POST(with{"target_id": …}, an ID or identifier) andDELETE …/{targetId}on…/documents/{id}/requirements,…/constraintsand…/test-casesread and change the links (owner).GET …/requirements/{id}/documents,…/constraints/{id}/documentsand…/test-cases/{id}/documentsread them from the other side.
A single requirement, constraint or test case (GET …/requirements/{id} and so on) names its documents
in documents; for a token without documents:read, only their identifiers.
Specification export
GET /api/v1/products/{productId}/specification (viewer role; tokens need structure:read and
backlog:read) returns the product’s specification as Markdown (text/markdown), not wrapped in
data: the structure, constraints and requirements, and with ?tasks=true the tasks. For a token with
documents:read it also lists the current documents, with ?drafts=true the drafts too, each with the
file name its body is exported under; the bodies come from the documents endpoints. The document is
ordered by stable keys and contains no timestamps, so an unchanged product exports identically. Errors
are JSON as usual.
Test runs
POST /api/v1/products/{productId}/test-runs (tester role) takes
{
"source": "go-test",
"campaign_id": "",
"environment": "linux-amd64",
"revision": "4161949",
"branch": "main",
"dry_run": false,
"results": [
{"test_identifier": "pkg.TestA", "status": "passed", "duration_ms": 12},
{"test_identifier": "pkg.TestB", "status": "failed", "output": "b_test.go:9: got 1, want 2"}
]
}
and answers with recorded, campaign, environment, a summary (counts per status, per outcome, defects_created, defects_reused, total) and per-test results with outcome (updated, unchanged, unknown_test_case, not_in_campaign, environment_required, not_recorded), defect (created, reused) and the defect’s defect_id and defect_identifier. Uploads are limited to 32 MiB and 50,000 results; statuses are passed, failed and skipped.
Audit trail (administrators)
The audit trail of every product, including deleted ones, is open to administrators (admin:read,
purging admin:write). The filters product_id, entity_type, user_id, action, from and to
(dates as YYYY-MM-DD or RFC 3339; to includes the whole day) apply to the list and the export.
GET /api/v1/admin/audit[?limit=&offset=]lists entries, newest first, as{"entries": […], "total": n, "limit": …, "offset": …};limitdefaults to 50, at most 500.GET /api/v1/admin/audit/export?format=jsonl|csvdownloads the matching entries, oldest first, as an archive file.GET /api/v1/admin/audit/deleted-productslists deleted products whose history is still kept, with their name and number of entries.DELETE /api/v1/admin/audit/products/{productId}purges the history of a deleted product and answers with{"purged": n}. A product that still exists keeps its history (409 PRODUCT_EXISTS). The purge itself is recorded as one entry with the actionpurge_audit.