Configuration comes from process environment variables. docker-compose.yml
supplies safe local defaults and can read a root .env; direct development must
set the variables in each process environment.
Which file a process reads is the part that catches people out, because the root
.env is the one npm run setup creates and asks you to edit — and it is not
read by the servers:
| Reader | Reads |
|---|---|
docker-compose.yml |
The root .env, and only for the three names it substitutes (ADMIN_TOKEN, SESSION_PASSWORD, CHECKOUT_SESSION_PASSWORD) |
setup, migrate, admin:create, run-local.bat |
The root .env, then .env.local. Neither overrides a variable already exported in the shell |
seed, smoke, smoke:stack, smoke:landing, docs:check |
Only the process environment |
| API and worker | <working directory>/.env, which under the workspace scripts is apps/api/.env — never the root .env |
| Console and checkout | apps/<app>/.env in nuxt dev only. The built server (node .output/server/index.mjs, and therefore Compose) reads only the process environment |
So npm run dev:api with a root .env full of values fails with ADMIN_TOKEN is required: export the variables in that shell, or put them in apps/api/.env.
And a NUXT_* value that works in nuxt dev because it sits in
apps/console/.env will be silently absent once the same application is built.
Never copy values from a real .env into documentation or commits. The examples
below are placeholders or local-only defaults.
#Compose-facing variables
These are the variables a root .env should normally override:
| Variable | Required | Purpose | Safe example |
|---|---|---|---|
ADMIN_TOKEN |
Yes outside Compose defaults | Root bootstrap/CI credential; minimum 16 characters | replace-with-random-hex |
SESSION_PASSWORD |
Recommended | Seals the console cookie in Compose | replace-with-32-plus-random-characters |
CHECKOUT_SESSION_PASSWORD |
Recommended | Seals the separate checkout cookie | different-32-plus-random-characters |
Compose injects internal service URLs itself. Its readable default credentials are for a private development machine only.
#API process
| Variable | Required | Default | Purpose |
|---|---|---|---|
ADMIN_TOKEN |
Yes | none | Bootstrap credential; API startup fails if absent/short |
PORT |
No | 8080 |
HTTP port |
DATABASE_URL |
No | local PostgreSQL on 54329 |
MikroORM connection |
MIKRO_ORM_DEBUG |
No | false |
Enables ORM debug output when exactly true |
DEFAULT_SITE_ID |
No | MLA |
Mercado Pago default site/currency mapping |
API_PUBLIC_URL |
No | http://localhost:8080 |
Public API origin for PayPal resource links and webhook certificate URL |
CHECKOUT_PUBLIC_URL |
No | http://localhost:3001 |
Origin used in Stripe redirects and PayPal approval links |
ALLOW_CONTRACT_DOWNLOAD |
No | false |
Opt-in guard for the contract importer |
MERCADOPAGO_CONTRACT_URL |
No | official allow-listed GitHub raw URL | Import source override within importer rules |
The HTTP API needs no queue at all. Webhook events are committed to PostgreSQL
and the worker dispatches them later, through whichever transport
WEBHOOK_QUEUE_DRIVER names.
#Worker process
The worker needs the same DATABASE_URL and ADMIN_TOKEN because it boots the
shared Nest context for timed transitions.
It also chooses a webhook transport. WEBHOOK_QUEUE_DRIVER takes postgres
(the default — the outbox carries its own deliveries and nothing else has to be
running), mongo (MONGO_URL, MONGO_DB; a standalone mongod is enough, no
replica set) or bullmq (REDIS_URL), which is what docker-compose names.
| Variable | Required | Default | Purpose |
|---|---|---|---|
WEBHOOK_QUEUE_DRIVER |
No | postgres |
postgres, mongo or bullmq |
REDIS_URL |
For bullmq |
redis://localhost:6389 |
BullMQ connection. run-local.bat derives it from REDIS_PORT, so the port it checks and the port the worker dials cannot disagree |
MONGO_URL |
For mongo |
mongodb://localhost:27017 |
Standalone mongod is enough |
MONGO_DB |
No | payment_emulator |
Database holding webhook_jobs |
RUNTIME_OUTBOUND_ENABLED |
For delivery | false |
Must equal true before webhook HTTP is allowed |
WEBHOOK_ALLOWED_HOSTS |
No | localhost,127.0.0.1,host.docker.internal |
Exceptions: hosts allowed in spite of resolving to an internal address. A public domain needs no entry |
OUTBOX_INTERVAL_MS |
No | 1000 |
Outbox polling interval |
TIMED_INTERVAL_MS |
No | 1000 |
Timed payment transition interval |
SETTLEMENT_INTERVAL_MS |
No | 60000 |
Reserve release sweep interval |
Allowed hosts are comma-separated hostnames, not URL patterns. The worker also
requires http: or https:.
#Console process
Nuxt runtime overrides must use Nuxt's NUXT_ names:
| Variable | Required | Default | Exposure |
|---|---|---|---|
NUXT_API_URL |
No | http://localhost:8080 |
Server only; NestJS origin |
NUXT_ADMIN_TOKEN |
Needed for bootstrap fallback | empty | Server only; root operator token |
NUXT_SESSION_PASSWORD |
Yes | empty | Server only; console cookie secret, 32+ chars |
NUXT_PUBLIC_CHECKOUT_URL |
No | http://localhost:3001 |
Browser-visible address |
NUXT_PUBLIC_API_PUBLIC_URL |
No | http://localhost:8080 |
Browser-visible address used for integration instructions |
NUXT_PUBLIC_SITE_URL |
No | empty | Public origin of the deployment. Sets the canonical URL, the hreflang alternates, the Open Graph image and the addresses in robots.txt and sitemap.xml. Left empty, each request answers for its own host, which is what a local install needs |
Normal proxied requests use the signed-in person's API session. The admin token
remains only as a server-side bootstrap fallback and must never be put under
runtimeConfig.public.
Documentation is built from local Markdown and served at /docs/es and /docs/en.
It requires no GitHub credentials, public repository URL or published branch.
See the documentation portal for publishing and validation.
#Checkout process
| Variable | Required | Default | Exposure |
|---|---|---|---|
NUXT_API_URL |
No | http://localhost:8080 |
Server only |
NUXT_SESSION_PASSWORD |
Yes | empty | Server only; checkout cookie secret, 32+ chars |
Use a different NUXT_SESSION_PASSWORD from the console. The cookies also have
different names (emulator_console and emulator_checkout).
#Windows without Docker (run-local.bat)
The launcher reads the root .env and .env.local, then fills in what is
missing. Its defaults are not always the code's defaults, because it runs against
a PostgreSQL you installed rather than the one Compose publishes:
| Variable | Default in run-local.bat |
Purpose |
|---|---|---|
PG_PORT |
discovered, then written to .env.local |
The port the local PostgreSQL answers on |
PG_CANDIDATES |
5432 5433 5434 5435 54329 |
The ports setup probes, newest instance wins |
PGSUPERUSER / PGSUPERPASSWORD |
postgres / postgres |
Only used by setup, to create the payment role and the database |
DATABASE_URL |
postgresql://payment:payment@localhost:5432/payment_emulator |
The local port, not the 54329 Compose publishes. A port inside this URL wins over PG_PORT |
API_PORT |
8080 |
Also sets API_URL; a PORT in the environment is read into it |
CONSOLE_PORT / CHECKOUT_PORT |
3000 / 3001 |
Also set CONSOLE_URL and CHECKOUT_URL |
REDIS_PORT |
6379 |
The locally installed default, not Compose's published 6389. REDIS_URL is derived from it |
MONGO_PORT |
27017 |
MONGO_URL is derived from it |
WEBHOOK_ALLOWED_HOSTS |
localhost,127.0.0.1 |
Without host.docker.internal: there is no container to reach back into |
ADMIN_TOKEN |
the Compose development token | The API refuses to start without one, so the launcher supplies it |
NUXT_SESSION_PASSWORD / CHECKOUT_SESSION_PASSWORD |
the Compose development secrets | Different values for the two surfaces |
run-local.bat doctor prints the ports and the chosen transport without
changing anything, which is the fastest way to see what these resolved to.
#Script-only variables
| Variable | Script | Default |
|---|---|---|
API_URL |
setup, admin creation, seed and stack smoke | http://localhost:8080 |
ADMIN_EMAIL |
admin creation | asked for |
ADMIN_PASSWORD |
admin creation | asked for |
ADMIN_NAME |
admin creation | the part of the address before @ |
CONSOLE_URL |
stack smoke and landing smoke | http://localhost:3000 |
CHECKOUT_URL |
stack smoke | http://localhost:3001 |
SMOKE_EMAIL |
stack smoke | smoke-operator@example.test |
SMOKE_PASSWORD |
stack smoke | smoke-operator-password |
SEED_ADMIN_EMAIL |
seed | operadora@example.test |
SEED_PASSWORD |
seed | emulator-demo-password |
All of these except the landing smoke also read ADMIN_TOKEN. The seed and the
stack smoke default it to the Compose local default; if Compose is overridden,
pass the same value to the script process. setup and admin:create have no
such default — they read .env and .env.local, and refuse rather than guess.
#Root commands
| Command | Does |
|---|---|
npm run setup |
The clean installation: dependencies, build, database, migrations and the administrator. --docker/--local, --seed, --reset, --no-admin, --yes |
npm run admin:create |
One administrator, through POST /auth/bootstrap, against a running API |
npm run migrate |
Pending migrations. -- --down for one step back, -- --fresh for all of them and back again |
npm run seed |
The demo data, through the HTTP surfaces |
npm run smoke / npm run smoke:stack |
The offline smoke, and the one against a running stack |
npm run smoke:landing |
The public page against a running console: canonical, hreflang, structured data, robots.txt, sitemap.xml, the 404 and the noindex on everything behind the login. CONSOLE_URL points it |
npm run docs:check |
Local Markdown links and English/Spanish counterparts |
setup, admin:create and migrate read .env and then .env.local, and
neither file overrides a variable already exported in the shell. The seed, the
smokes and the documentation check read only the process environment — see the
table at the top.
#Execution modes
| Mode | Processes | Intended use |
|---|---|---|
| Full Compose | PostgreSQL, Redis, API, worker, console, checkout | Recommended onboarding and acceptance; the worker is pinned to bullmq there |
run-local.bat |
The four processes against a locally installed PostgreSQL | Windows without Docker; reads .env.local for machine settings |
run.bat |
Compose, wrapped | Windows with Docker; up, dev, infra, setup, admin, reset and the rest |
| Direct development | Dependencies in Compose; API/Nuxt development servers and compiled worker | Fast code iteration |
| API-only | PostgreSQL + API | Provider/control-plane work without webhook delivery |
| Checks | Typecheck, lint, Vitest, build, pure smoke | Local/CI verification |
#Contract download
Contract import is intentionally separate from runtime:
ALLOW_CONTRACT_DOWNLOAD=true npm run contract:import:mercadopago --workspace @payment-emulator/api
It uses an allow-listed official URL and records a checksum. Do not enable downloads on the API or worker runtime merely to run the emulator. Official snapshots for all three providers are present; see the contract inventory. Their presence does not imply complete runtime schema validation.