Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

RequirementTaskConstraint
What it isA statement of what the product must doWork that says nothing about the product: maintenance, tooling, researchA 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 whenIts definition of done holdsIt is finishedNever: it is met or violated, continuously
Moves through the workflowYesYesNo; it has a compliance state instead
Linked to capabilities and test casesYesNoCapabilities 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:

RoleCan
Product ownerEverything in the product: settings, team, structure, documents, backlog, campaigns
DeveloperCreate and edit test cases; comment; change the status of requirements, tasks and defects once they are activated
TesterRecord campaign results; create defects; comment on defects and test cases
ViewerRead 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.

FieldMeaning
Name, descriptionShown on the dashboard and the product overview
Estimation modeHow backlog items are estimated: story points, t-shirt sizes, hours, or disabled
Workflow modeKanban (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 (DF in DF-17), the document identifier prefix (DOC in DOC-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-12 appear 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, windows or firefox. 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:

  1. Under Invite New User, enter their email address and the role they should get.

  2. 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.

  3. Choose Send Invitation.

  4. 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:

  1. Create the components the product consists of.
  2. Create capabilities, each under a component or at product level.
  3. 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 sync can 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.

FieldMeaning
CategoryDecides the identifier prefix, such as FR or MT
Title, descriptionThe description is written in Markdown
Definition of doneRequirements only: when the requirement counts as fulfilled. Test cases can be linked to it
ParentBreaks work down: a requirement under a requirement, a task under either
StatusPosition in the workflow; new items get the product’s default status
MilestoneThe milestone this item contributes to
SprintScrum mode only: the sprint the item is planned into
PriorityA number; lower numbers mean higher priority
EstimateOnly when the product uses estimation, in the product’s unit
CoarseMarks 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.

FieldMeaning
Title, descriptionWhat is checked and how; for manual tests, the steps to follow
Execution modeManual (a person carries it out) or automatic (a test in code)
Test identifierAutomatic tests only: the name that test results report, such as a Go test function name. It connects CI results to the test case
Component, capabilityWhat part of the product the test case covers
Pyramid levelUnit, Integration, End-to-End, or the product’s own levels
EnvironmentsThe 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:

  1. 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.
  2. 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.
  3. 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:

StatusMeaning
PendingNot run yet; every result starts here
PassedThe check succeeded
FailedThe check failed; requires a defect
BlockedThe check could not be carried out, for example because something it depends on is broken
SkippedDeliberately not run in this campaign
Re-testThe 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:

RefHoldsUse it for
stableThe newest release; it moves only when a release is madeDeployments that update with each release, such as Coolify watching the branch
v0.9.1, …One release, unchangedDeployments pinned to a version, updated by hand
mainDevelopment work between releases, reporting v0.9.1+devDevelopment 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), and NEXMAP_ADMIN_USER / NEXMAP_ADMIN_PASSWORD for the first start; NEXMAP_PUBLIC_URL with the application’s domain; for PostgreSQL, NEXMAP_DB_DRIVER=postgres and NEXMAP_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 passes SOURCE_COMMIT to the build, the commit (v0.9.1+dev.<commit>). Do not set a VERSION variable: 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

VariableFlagDefaultImageMeaning
NEXMAP_HOST--hostlocalhost0.0.0.0Address to listen on. 0.0.0.0 listens on all interfaces
NEXMAP_PORT--port80808080Port to listen on
NEXMAP_PUBLIC_URL--public-urlemptyThe 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-proxiesloopback and private networksAddresses 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

VariableFlagDefaultImageMeaning
NEXMAP_DB_DRIVER--db-driversqlitesqlitesqlite or postgres
NEXMAP_DB_DSN--db-dsnnexmap.db/data/nexmap.dbFor 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

VariableFlagDefaultMeaning
NEXMAP_ENCRYPTION_KEY—none, requiredKey ring for encrypting personal data, as JSON. See Security

The server does not start without it.

First administrator

VariableFlagDefaultMeaning
NEXMAP_ADMIN_USER—adminUsername of the administrator created on first start
NEXMAP_ADMIN_PASSWORD—generatedPassword 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

VariableFlagDefaultMeaning
NEXMAP_SESSION_TTL--session-ttl24hHow long a web UI session lasts, as a Go duration (8h, 30m, 168h)
NEXMAP_COOKIE_SECURE--cookie-securetrueMark session cookies Secure, so browsers send them over HTTPS only. Set to false only for local development over plain HTTP
NEXMAP_PBKDF2_ITERATIONS--pbkdf2-iterations600000PBKDF2-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

VariableFlagDefaultImageMeaning
NEXMAP_UPLOAD_DIR--upload-dir./uploads/data/uploadsDirectory for uploaded attachments
NEXMAP_MAX_UPLOAD_BYTES--max-upload-bytes10485760 (10 MiB)Largest accepted attachment, in bytes
NEXMAP_ALLOWED_UPLOAD_TYPES--allowed-upload-typesempty: all typesComma-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.

Mail

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.

VariableDefaultMeaning
NEXMAP_MAIL_TRANSPORTempty: no mailsmtp, mailgun, sendgrid, mailpace, scaleway or lettermint
NEXMAP_MAIL_FROMnone, required with a transportSender, 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_HOUR100Most emails the server sends in an hour, all kinds together. A safety net; the limits below normally apply first
NEXMAP_SMTP_HOSTSMTP relay (transport smtp)
NEXMAP_SMTP_PORT587, or 465 with tls
NEXMAP_SMTP_TLSstarttlsstarttls: 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_PASSWORDempty: no loginCredentials for the relay (AUTH PLAIN, over TLS only)
NEXMAP_MAIL_API_KEYKey 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_DOMAINSending domain at Mailgun (transport mailgun)
NEXMAP_MAILGUN_REGIONusus or eu
NEXMAP_SCALEWAY_PROJECT_IDProject of the Transactional Email domain (transport scaleway)
NEXMAP_SCALEWAY_REGIONfr-parRegion 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

VariableFlagDefaultMeaning
NEXMAP_LOGLEVEL--log-levelinfodebug, info, warn or error
NEXMAP_LOGFILE--log-fileempty: stdoutWrite the log to this file (appended, mode 0600) instead of stdout

Logs are JSON, one entry per line.

Development

VariableFlagDefaultMeaning
NEXMAP_DEV_MODE—falseEnables 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 for true or 1; 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_SECURE at true. Browsers then send the session cookie over HTTPS only, and the server sends Strict-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-For header, 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 nexmapd on a private network, set NEXMAP_TRUSTED_PROXIES to 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 Origin header with the Host header, so the proxy has to pass the original Host on (Traefik and Caddy do; nginx needs proxy_set_header Host $host). Setting NEXMAP_PUBLIC_URL makes 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 get 403 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.

  • Usernames and email addresses are unique regardless of case: Admin cannot be registered next to admin, 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 account or admin scope.

  • 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_admin in 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_ATTEMPTS with a Retry-After header. 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 codes with 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 from NEXMAP_PUBLIC_URL if 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/users before 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.

Email

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:write or admin). 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-password answers 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_URL only, which is required with mail. A reset link built from the request’s Host header 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:

PartWhereNotes
DatabaseNEXMAP_DB_DSN: the SQLite file (container: /data/nexmap.db) or the PostgreSQL databaseEverything except attachment contents
AttachmentsNEXMAP_UPLOAD_DIR (container: /data/uploads)One directory per product, one file per attachment
Encryption keyNEXMAP_ENCRYPTION_KEYNeeded 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

  1. Stop nexmapd.
  2. Put the database back:
    • SQLite: replace nexmap.db with the backup file and delete any nexmap.db-wal and nexmap.db-shm left next to it, as they belong to the replaced database.
    • PostgreSQL: pg_restore --clean --if-exists --no-owner --dbname="$NEXMAP_DB_DSN" nexmap.dump drops the existing tables and restores those of the backup.
  3. Replace the attachment directory with the one from the backup.
  4. Make sure NEXMAP_ENCRYPTION_KEY contains the key (or keys) the backup was written with.
  5. 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.sh into ~/.local/bin, or /usr/local/bin when run as root. It tells you when that directory is not on your PATH.
  • install.ps1 into %LOCALAPPDATA%\Programs\nexmap, which it adds to your user PATH.

They read these environment variables:

VariableMeaning
NEXMAP_VERSIONVersion to install, e.g. v1.0.0; default: the latest release
NEXMAP_INSTALL_DIRDirectory 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

  1. In the web UI, open API Tokens in the user menu (/user/tokens) and create a token.

  2. Store it:

    nexmap login --server https://nexmap.example.com
    

    On a terminal, login prompts 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:

  1. Command line flags
  2. Environment variables
  3. .nexmap.json in the current directory or the nearest parent
  4. The user configuration file
SettingFlagEnvironment.nexmap.jsonUser configuration
Server URL--serverNEXMAP_SERVERserver—
Product ID--productNEXMAP_PRODUCTproduct—
Output format-o, --outputNEXMAP_OUTPUT—output
Token—NEXMAP_TOKEN—stored per server by login
Plain HTTP--allow-httpNEXMAP_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

CommandPurpose
login, logout, whoamiManage the stored token, show what is in effect
initWrite .nexmap.json; with --agents, set up AI agents
versionClient version and, if a server is configured, the server version
updateInstall the latest or a given release, see Updating
contextProduct briefing: structure, constraints, work in progress, defects, testing status
req, taskRequirements and tasks: list, show, status, create
constraintlist, show, create, update (e.g. --compliance met)
docDocuments: list, show, create, update, delete, link/unlink, revisions, diff, restore, get
componentProduct structure: list, create, update, delete
capabilitylist, show, create, update, delete, and link/unlink requirements and constraints
defectlist, show, status, create
testTest cases: list, show, sync, and link/unlink them to a requirement, capability or component
campaignstatus of the active testing campaign
commentadd a comment to any commentable item
utestRecord test results from CI, see CI integration
exportThe product’s specification as Markdown, see below
apiRaw REST call, see Scripting
agent-guide, mcpFor 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

FormatUse
-o tablePlain tables and records for people. The default
-o mdCompact markdown with the same headings every time; best for pasting into an AI agent’s context
-o jsonMachine-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 codeMeaning
0Success
1Usage or configuration error (bad arguments, no server configured, ambiguous reference)
2API error: server unreachable, authentication failed, or not permitted for your role
3Not found: the item does not exist or is not visible to you
4Tests 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

SituationEffect
Test passes / is skippedIts campaign result is set to passed / skipped; a defect on the result is cleared
Test failsResult 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 runNothing is written, so repeated CI runs do not flood the result history
Subtests (TestX/case) without their own test caseRolled up into TestX: any failure fails it, the failing subtests’ output goes into the defect
Test case runs per environmentNeeds --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 campaignNot recorded, no defect; failures are still listed in the build log
No active campaignThe 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

  1. Create a token for CI under API Tokens in the user menu (/user/tokens) with the CI ingestion preset (tests:write only), 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.
  2. Register the tests as automatic test cases with a test identifier. nexmap test sync creates the missing ones from a report (developer role):
    go test -json ./... | nexmap test sync --dry-run   # review first
    go test -json ./... | nexmap test sync
    
    --component or --capability files the new test cases in the product structure, --pyramid-level unit sets their level and --req FR-12 links 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-style here as for nexmap utest. New test cases join campaigns created afterwards; an active campaign keeps the scope it was created with.
  3. Check the mapping before wiring CI: go test -json ./... | nexmap utest --dry-run shows how each test maps to a test case and what would change.
  4. 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’s PATH. The update notice stays off in CI.
  5. Configure the job with NEXMAP_SERVER, NEXMAP_PRODUCT and the secret NEXMAP_TOKEN. The product may come from a committed .nexmap.json instead (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 sends NEXMAP_TOKEN to a server taken from .nexmap.json.

Options

FlagMeaning
--format auto|go-json|go-text|junitReport format; auto sniffs the input (JSON events → go-json, XML → junit, else go test -v text)
--identifier-style package|namepackage (default): pkg/path.TestName for Go, classname.name for JUnit; name: the bare test name. Match whatever your test cases use
--env NAMERuntime environment of this run (case-insensitive); unknown names are rejected
--campaign NAMERecord in this campaign instead of the active one (must be active)
--revision, --branchDefault from GITHUB_SHA/GITHUB_REF_NAME, CI_COMMIT_SHA/CI_COMMIT_REF_NAME, … or git rev-parse
--dry-runEvaluate and print the mapping; write nothing
--exit-codeExit 4 if any test (or a package build) failed
--quietDo 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 nexmap entry is left alone unless you pass --force.

  • appends the agent guide (nexmap agent-guide) to CLAUDE.md if that exists, otherwise to AGENTS.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 the get_context tool) for a product briefing, then requirements, tasks, constraints, structure, defects, test cases and the campaign status as needed. nexmap export (or the export_specification tool) 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, the get_document tool). 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 (the document_diff tool) 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:

ActionRole needed
Set the status of a requirement, task or defectdeveloper, and only on items a product owner has activated (kanban: by moving them into an active column; scrum: by planning them into a sprint)
Commentdeveloper; tester on defects and test cases
Report a defecttester
Link a test case to a requirement, capability or component; create test cases from a test rundeveloper
Create a taskproduct owner
Create or update a constraint, component or capability; link capabilitiesproduct owner
Create, update, link or restore a documentproduct 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 read or write; write includes read. Without scopes, a token gets write on every product scope, but never account or admin.

    ScopeCovers
    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 tokens
    admin/admin/*; without it, an administrator’s token has no administrator rights

    A request needs read on every resource named in its path, and write on the first one if it changes data: POST /products/{p}/capabilities/{id}/requirements needs structure:write and backlog:read. Side effects belong to the request’s own scope, so uploading a test run needs only tests:write although it files defects. Linking a document to a requirement or constraint needs documents:write and backlog:read, to a test case documents:write and tests:read. Comments and attachments also need read on the item they belong to. The audit trail needs read on every product scope. GET /user/me is open to every token. A missing scope is answered with 403 and the code TOKEN_SCOPE.

  • Role cap. The token acts with the lower of its user’s role and the cap (developer, tester or viewer).

  • Products. The token reaches only the listed products, and GET /products lists 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’s url. With "delivery": "email" (only if the server sends mail), Nexmap emails the link to that address instead, and the answer carries neither url nor raw_token. Invitation emails are limited per inviter and per address (429 TOO_MANY_ATTEMPTS); if the email cannot be sent, the answer is 502 MAIL_NOT_SENT and 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-cases with {"test_case_ids": […]} and DELETE /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_title and the environment_name they 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 pending or retest.
      • Closed campaigns keep everything.
    • Kept results then have test_case_id null, or environment_id empty, but keep their title, environment name and history.
  • 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}/documents lists them, without bodies unless ?body=true; filter with state, format, component_id, capability_id, milestone_id and q (title or identifier).
  • POST /api/v1/products/{p}/documents (owner) creates one from title, body, format (markdown, the default, or text, json, yaml, xml, proto, sql, graphql), state (draft, the default, current or superseded), and optionally component_id or capability_id (not both), milestone_id and, 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, PATCH and DELETE /api/v1/products/{p}/documents/{id}. A PATCH that changes the title, body or format creates a new revision; revision is the number of the latest one.
  • GET …/documents/{id}/raw[?revision=n] returns the body itself, always as a text/plain attachment named like DOC-3.json, so a browser never renders it.
  • GET …/documents/{id}/revisions lists the revisions, GET …/revisions/{n} returns one with its body, GET …/diff?from=&to= compares two (by default the latest with the one before) as unified text and as lines, and POST …/restore with {"revision": n} (owner) makes an earlier content current again as a new revision.
  • GET, POST (with {"target_id": …}, an ID or identifier) and DELETE …/{targetId} on …/documents/{id}/requirements, …/constraints and …/test-cases read and change the links (owner). GET …/requirements/{id}/documents, …/constraints/{id}/documents and …/test-cases/{id}/documents read 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": …}; limit defaults to 50, at most 500.
  • GET /api/v1/admin/audit/export?format=jsonl|csv downloads the matching entries, oldest first, as an archive file.
  • GET /api/v1/admin/audit/deleted-products lists 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 action purge_audit.