Skip to content
Developer build

Developer build

MicroB2B is compiled in two variants of the same code, chosen by the Go build tag developer when the binary is built, never by an environment variable, flag, setting or request. The release build is what is installed and what the rest of the documentation describes. The developer build is for working on MicroB2B itself: on a developer’s own machine, in tests and in CI. It is never installed anywhere someone else can reach it.

Release buildDeveloper build
Made byjust build-release, just container-release, go build ./cmd/microb2bjust build, just run, just container, go build -tags developer ./cmd/microb2b
Web UISign-in required (see Sign-in)The same login page, plus a Sign in as developer button that anybody can press; every page shows the banner Developer build: anybody can sign in as the developer and a startup warning is logged
REST APIA credential is required: an API token or an identity provider’s access tokenA request without an Authorization header is accepted as the user developer; a credential it is given is checked exactly as in a release build, so tokens can be tried out on it
Required configurationPUBLIC_URL, SESSION_KEY, SECRETS_KEYNone (see Configuration)
API server listens onEvery interface, port HTTP_PORTLoopback only (127.0.0.1, and ::1 where the host has it), unless DEVELOPER_LISTEN_ADDRESS names another address (see Listen address)
Cookies__Host-mb2b_session, __Host-mb2b_login, __Host-mb2b_oidc, __Host-mb2b_csrf, all Securemb2b_session, mb2b_login, mb2b_oidc, mb2b_csrf, without Secure, because it is served over plain http, also to other machines
--reset-db, --fixtures, --pprofDo not existExist (see Development flags, Profiling)
Database page (/ui/database)Does not existExists (see Database console)

The developer user is an ordinary user record, developer, created by the first press of the button, enabled and with password login off; it is listed on Access > Users and named in the audit log like any other.

go test ./cmd/microb2b builds both binaries and fails if the release one contains any developer code or starts without authentication configured.

Configuration

The developer build needs no configuration to start:

  • PUBLIC_URL is optional, and takes http on any host. Without it, setup links are shown and printed as paths and no identity provider is offered (their redirect URIs are made of it).
  • SESSION_KEY is optional. Without it the session cookies are sealed with a fixed developer key, so a session outlives a restart.
  • SECRETS_KEY is optional. Without it the secrets and private keys are sealed with a fixed developer key; the log says so, and it protects nothing. Fixtures write secrets in the clear and they are sealed when the fixture is loaded.

Listen address

Anybody can sign in to the developer build as the developer and its REST API takes every request without a token, so its API server (web UI and REST API) listens on loopback only: localhost:8080 works on the machine itself, and no other machine on the network reaches it. DEVELOPER_LISTEN_ADDRESS opens it to one other address: an IP address such as 192.168.1.20, or 0.0.0.0 (or ::) for every interface. A value that is not an IP address stops startup with an error naming the variable, and a set value is logged as a warning at startup. A release build does not read the variable.

Inside a container, loopback is the container’s own, so a published port would reach nothing: set DEVELOPER_LISTEN_ADDRESS=0.0.0.0 there and publish the port on the host’s 127.0.0.1 only. just container builds the developer image microb2b:latest:

docker run -d --name microb2b \
  -p 127.0.0.1:8080:8080 -p 8081:8081 \
  -e DEVELOPER_LISTEN_ADDRESS=0.0.0.0 \
  -e DB_URL=file:/opt/microb2b/data/microb2b.db \
  -v microb2b-data:/opt/microb2b/data \
  -v microb2b-payloads:/opt/microb2b/payloads \
  microb2b:latest standalone

This is the API server’s address alone. An AS2 receiver listens on the Bind address and Port of its own instance configuration in both builds, because partners must reach it.

If the web UI works on localhost:8080 but not from another machine, or a container’s published port answers nothing, this is why.

Development flags

standalone has two flags in the developer build: --reset-db drops and recreates every table from the current models, and --fixtures <file> loads a YAML fixture such as test/data/fixture.yaml instead of seeding a new database. Both destroy data and exist only for local work; a release binary rejects them as unknown flags. A fixture is checked like every record: a value the form and the REST API would refuse stops startup with stored records break the rules of their forms and REST API, naming the record, the field and the rule.

just run builds, copies test/data/* to target/ and starts standalone mode from there with both flags: the database is dropped and rebuilt from target/fixture.yaml on every run, producing a ready-made organization, partner, plugins and routes. just sql and just list-messages open target/microb2b.db of that run. Never use the flags on data you want to keep.

Database console

The developer build has a Database page in the System section of the menu (/ui/database); a release binary does not contain it. On the left are the tables of the database: choosing one shows its columns (type, nullable, primary key) and its row count, and puts SELECT * FROM "<table>" LIMIT 100 in the editor. The editor runs one SQL statement at a time:

  • A statement that returns rows (a query, PRAGMA, EXPLAIN, ... RETURNING) shows them as a table: at most 200 rows, at most 200 characters of a value (shortened values end in …), and fewer rows when the values add up to 256 KiB; a result with more rows says it was truncated. NULL is a grey badge, an empty string an empty cell, and bytes that are not text are shown as 0x.... Whether a statement returns rows is what the database reports, not a guess from its first word.
  • Any other statement shows how long it took and, on SQLite, how many rows it changed (PostgreSQL does not report the count through the driver’s query path).
  • An error is shown with the statement still in the editor, to fix and run again.

Each statement runs in a transaction of its own on one connection: committed when it succeeds, rolled back when it fails or takes longer than 10 seconds. So nothing is left half done, and BEGIN/COMMIT and statements that cannot run in a transaction (VACUUM) are not supported. Text holding more than one statement is refused before anything runs. The statements run with the application’s full database rights.

The last 50 statements run are listed under Recent statements, newest first, a statement run again moving to the top; choosing one puts it in the editor. They are kept in the console’s own table, dbconsole_recent_statements, shared by everyone using the instance, so the list works without JavaScript and survives a browser change; --reset-db empties it. Statement texts and result values are never logged.

Load testing

just test-load makes MicroB2B test against itself: it builds the developer binary and runs two standalone instances on one machine, A (file poller, AS2 sender) and B (AS2 receiver, file writer), each in a scratch directory of its own with its own database (a SQLite file, or a PostgreSQL database with -postgres), API port and receiver port (free ports, chosen per phase). Nothing it starts touches another running instance. test/selftest-load holds the tool; flags follow the recipe, for example just test-load -n 2000 -size-kib 100 -profiles secure,async.

FlagDefaultMeaning
-n500Messages (files) per profile
-size-kib10Payload size; the content is random, so compression cannot flatter the result
-profilesplain,signed,secure,asyncProtection profiles, each a phase on a fresh pair of instances (below)
-sender-concurrencymodule defaultA’s max_concurrent_deliveries
-receiver-concurrencymodule defaultB’s max_concurrent_requests
-poll-interval1A’s poller interval in seconds
-sigterm-afteroffSend SIGTERM to the sending process A once this many messages are Delivered on it, with a backlog still to go, and start it again on its database (below)
-scalingoffAfter the profiles, run the first one with A’s max_concurrent_deliveries at 1 and at 8 and fail when 8 is clearly slower (below 0.7 of the serial rate)
-timeout10mLongest a phase may take
-profileoffStart both instances with --pprof, profile them during the run and print the top functions (below); implies -keep
-postgresoff (SQLite)URL of a PostgreSQL server whose user may create databases, for example postgres://postgres:[email protected]:5432/postgres?sslmode=disable; each instance of each phase gets the database selftest_<profile>_<a|b> on it, dropped and created when the phase starts and left behind afterwards
-keep, -workdirKeep the scratch directory (databases, logs, payloads), or create phases in a directory of your choice

The profiles: plain (no signature, encryption, compression or MDN), signed (SHA-256 signature, synchronous signed MDN), secure (compressed, AES-256-CBC, SHA-256 signature, synchronous signed MDN) and async (SHA-256 signature, asynchronous signed MDN). Each side has a self-signed certificate with a private key, loaded with a fixture.

A phase stages all files in A’s outbox/<partner>/ before A starts, so the poller’s first scan queues them at once and the measured rate is not bounded by the poll interval. It waits until A holds every message as Delivered and B’s writer has written every file, then stops both processes with SIGTERM and judges the run from the two databases and B’s output directory: exactly one Delivered message per file on A, exactly one message per file on B, none in Error, exactly one output file per input and every output file byte for byte (SHA-256) the original. Any mismatch is listed and the exit status is non-zero.

With -sigterm-after N, A is stopped with SIGTERM as soon as N messages are Delivered on it while the rest are still queued, in flight or in the outbox; it is started again on the same database and directories (no reset, no fixture) and the phase carries on. The run fails unless the stop left no delivery failed, there was a backlog to interrupt, A exited cleanly, and every staged file ends as exactly one Delivered message on A and one Delivered (written) message on B with B’s output equal to the input. B’s rows for a resend it recognized as a repeat of an accepted message (Repeat of message ...) are counted and reported, not mixed with the messages its writer saw; the writer never gets a Message-ID twice. just test-load -n 300 -profiles secure -sigterm-after 60 is the short form.

Reported per phase: wall time from the first file queued to everything delivered and written, messages and MiB per second, the time to 50% and 90% delivered, the per-message time from the poller queuing a file to the sender’s POST (and a synchronous MDN) completing, the sender’s own HTTP exchange time and the time to B’s written file (percentiles), peak resident memory and CPU time of each process from /proc, and the WARN and ERROR lines of both logs by message (the developer build’s startup banner is excluded). Per-message times come from the log lines, which carry milliseconds; the database stores timestamps to the second. Delivery of an asynchronous MDN is not logged, so for async the Delivered moment is the run’s own observation of the message count through the REST API (polled every 100 ms).

Profiling

The developer build serves pprof profiles on request: microb2b standalone --pprof 127.0.0.1:6060 (also on persistence) starts a listener of its own that serves /debug/pprof/ (profile?seconds=N for CPU, heap, allocs, goroutine, mutex, block, trace). It is not on the API server, it accepts a loopback IP address only (a host name or any other address stops the start with an error), and without the flag nothing listens. The flag also turns on mutex and block sampling, which a normal run leaves off. A release build has no such flag. For example go tool pprof -top -cum http://127.0.0.1:6060/debug/pprof/profile?seconds=10.

just test-load -profile does this for both instances of the load test. During the run it takes one-second CPU profiles back to back from each instance, with a goroutine summary after each; at the end it takes heap, allocs, mutex and block profiles. They are written to <scratch>/<profile>/{a,b}/profiles (cpu-NNN.pb.gz, goroutines-NNN.txt, heap.pb.gz, …) and the report prints the top 25 functions by flat and by cumulative CPU, the mutex and block delay the run added, and allocated and in-use memory. go tool pprof -top target/microb2b <dir>/cpu-*.pb.gz merges the CPU chunks.

Checking setups against real software

These scripts start a developer build and are opt-in, not part of go test:

  • test/oidc/keycloak-check.sh (just test-keycloak) starts a Keycloak in docker, imports a small realm (test/oidc/realm-microb2b.json), starts a developer build, configures the provider through its REST API and drives login (which creates the user) and logout like a browser would, then starts the release build on the same database and checks its Secure login cookie and API calls with a client credentials token.
  • test/reverse-proxy/check.sh (just test-reverse-proxy) extracts the Caddy and nginx recipes from the reverse proxy page, starts each proxy in front of a developer build and checks the UI, an event stream that arrives unbuffered, a large upload, an AS2 body that passes and one over the limit that the proxy refuses.