First boot and verification

Start the three processes, sign in as the first administrator, activate the licence, and prove that every subsystem is actually alive before you hand the site to anyone.

15 min readUpdated 3 August 2026pm2, logs, licence, verification

The installer finishing is not the same as the platform working. This page is the checklist that turns "it installed" into "it runs", and the place to start when the answer is neither.

Work through it in order. Each check proves one layer, and a failure at a lower layer explains every symptom above it — chasing a blank browser page is wasted time if Redis is not answering.

What a running install looks like

A production deployment is three PM2 processes, started together by pnpm start. They are defined in production.config.js at the project root.

Process What it is Port Who talks to it
backend The API and every WebSocket endpoint NEXT_PUBLIC_BACKEND_PORT, default 4000 your reverse proxy, on /api
frontend Next.js, serving pages and uploads 3000 your reverse proxy, on /
cron The scheduler. Runs jobs, serves no traffic 4001 nothing

Three facts about that table cause most of the confusion on day one.

The frontend port is hardcoded to 3000 in the PM2 config. A PM2 env block overrides the inherited environment, so setting NEXT_PUBLIC_FRONTEND_PORT in .env does not move it.

The cron process binds 4001 only to avoid a port clash with the backend. It registers no application routes at all — just / and /api, which answer with a plain "Backend Service" page. Never point a load balancer or a health check at it.

Ports 4000 and 4001 must not be reachable from the internet. The installer's firewall step opens ssh, http, https and 3000, and never opens 4000 — but it also writes no reverse-proxy configuration, so the firewall is the only thing standing between the open internet and an unauthenticated backend if you opened those ports by hand.

Start it

cd /path/to/your/install
pnpm start

That command does three things in sequence, and each can stop the chain:

  1. scripts/maintenance-off.js removes the maintenance server and waits for ports 3000 and the backend port to go quiet.
  2. scripts/reconcile-scheduler.js compares what PM2 is currently running against the scheduler layout in production.config.js, and deletes any of backend, frontend, cron whose role disagrees. PM2 skips an app it has already seen and keeps the environment that app was created with, so without this step an upgraded install can end up with two processes scheduling the same jobs.
  3. pm2 start production.config.js --env production.

Then confirm PM2 agrees:

pm2 list

You want exactly backend, frontend and cron, all online, all with a low restart count. A maintenance entry means the platform is deliberately down — see the accordion at the bottom of this page.

The installer runs pm2 startup but never pm2 save, so the boot hook comes back to an empty process list. Once pm2 list looks right, run pm2 save yourself. Skip it and the machine comes back from a reboot with nothing listening.

The other lifecycle commands:

pnpm stop      # stops all three and puts up the 503 maintenance server
pnpm restart   # stop, then start
pnpm stop:all  # removes everything including maintenance — ports simply close

There is no systemd unit. The installer's closing summary advertises systemctl start|stop|restart bicrypto, but the same installer deliberately deletes /etc/systemd/system/bicrypto.service if it finds one — the old unit was Type=simple with Restart=always around a command that correctly exits in a few seconds, so systemd restarted the whole platform every ten seconds forever. Use the pnpm commands.

Read the logs

Every question below is answered faster from a log than from a browser.

production.config.js sets no out_file or error_file, so PM2 writes to its own default directory — $HOME/.pm2/logs/ for the user that started it. Ask PM2 rather than guessing:

pm2 describe backend | grep -i "log path"

Day to day you want the streaming view. Each process is named, so you can watch one at a time:

pm2 logs backend --lines 200
pm2 logs frontend --lines 200
pm2 logs cron --lines 200
pm2 logs --lines 100

A healthy backend boot prints a tagged line as each subsystem comes up — REDIS, CUSTOM_CHAINS, ROLES, ROUTES and the rest — and closes with a green banner:

✔ Web tier ready on port 4000 (3.1s)

The scheduler prints the same phases plus CRON, and closes with Scheduler ready on port 4001. If you see that line on the backend process instead, the two apps have swapped ports and something in PM2's stored environment is stale — pnpm restart re-runs the reconciler.

Two other logs exist only on installer-built machines and are worth knowing about when the failure happened before you ever ran pnpm start:

  • /var/log/bicrypto-installer.log — the entire installer run, stdout and stderr both.
  • /tmp/bicrypto-startup.log — the output of the one pnpm start the installer performs itself at the end.

Verify each subsystem

Run these on the server, in this order.

  1. Redis — a hard dependency, not a cache. Sessions, rate limits, locks, the BullMQ scheduler and cross-process settings invalidation all live in it, and the old in-memory fallback has been removed.

    redis-cli ping

    Expect PONG. If the backend cannot reach Redis it prints a boxed message naming the host, port and database it tried, then exits 78 — which the PM2 configs list in stop_exit_codes, so the app stops with the message on screen instead of restart-looping it away.

  2. Database — the installer never installs MySQL or MariaDB. It only prompts for credentials and connects, and its own connection test is broken (it reads the exit status of the wrong command), so a wrong password sails through the install and only surfaces here.

    mysql -u "$DB_USER" -p -h "$DB_HOST" -P "$DB_PORT" "$DB_NAME" -e "SELECT COUNT(*) FROM user;"

    A non-zero count means the schema imported and the seeders ran. An "unknown database" or access-denied error means .env and the server disagree.

  3. Backend API/api/settings is unauthenticated by design (it feeds the public site's branding), which makes it the readiness probe the platform's own update tooling uses.

    curl -s http://127.0.0.1:4000/api/settings | head -c 200

    Expect JSON containing settings and extensions. Connection refused means the backend is not listening — go back to pm2 logs backend.

  4. Scheduler process — the cron process answers on 4001 even though it serves no application routes.

    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4001/api

    200 means the process is up. That is not the same as the scheduler being alive — step 7 checks that properly.

  5. Frontend

    curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:3000

    Expect 200 or a redirect to a locale-prefixed path. A 500 here with a healthy backend usually means the Next build is missing or stale — rebuild with pnpm build:frontend.

  6. Through the proxy — the only check that reflects what a visitor gets.

    curl -s https://your-domain.example/api/settings | head -c 200

    If the local call in step 3 works and this one returns your site's 404 page, your reverse proxy has no location /api block. Next.js does not proxy /api in production; those rewrites are development-only and sit below an early return in frontend/next.config.js.

  7. The scheduler is actually scheduling — sign in and open System → System Monitoring → Scheduled Tasks (/admin/system/cron).

    The job list alone cannot answer this: every entry comes from a registry that exists identically in all three processes, so a scheduler that died an hour ago still renders a full page of jobs with plausible run times. The panel on that screen reads a Redis heartbeat instead — written every 15 seconds with a 90-second expiry — and reports running, missing, stale, duplicate or unknown. duplicate is worse than missing: two processes registering the same jobs means money-moving jobs can run twice over the same rows.

  8. WebSockets — every realtime endpoint lives under /api and connects to wss:// on your own host with no port, so it rides the same proxy rule as the REST API. The Scheduled Tasks screen from step 7 is itself driven by a WebSocket, so if that page updates live without a reload, upgrades are passing through your proxy correctly. A trading page with frozen prices and a working page load is the same failure seen from the other end.

Sign in as the first administrator

The seeder creates one account, and only on a database with no users at all:

superadmin@example.com
12345678

Sign in at /login, then change the password immediately from the user profile screen. That password is published in the installer output and in this documentation; on a public host it is a live vulnerability until you change it.

You can avoid it existing at all. Set both variables in .env before the first pnpm seed and the seeder uses them instead:

SUPERADMIN_EMAIL="you@your-domain.example"
SUPERADMIN_PASSWORD="a long random string"

The seeder writes a marker row into the settings table (superAdminSeeded) once it has run, and every later pnpm seed — including the one inside pnpm updator — reads that marker and does nothing. That is deliberate: without it, deleting the default administrator brought it straight back on the next update, which reads as an account you cannot remove. It also means that if you delete the default account and lose your own, no seeder will hand you a new one.

The seeded roles are Super Admin, Admin, Support and User. Super Admin bypasses every permission check by name, and a handful of settings — the withdrawal approval switches, the transfer fees, the withdrawal 2FA policy — are writable by Super Admin only. Grant other roles their keys under Users → Roles & Permissions.

Activate the licence

Until the licence is activated, the backend answers 403 on almost every API route. A short exempt list keeps the install recoverable: /api/auth, /api/user/profile, /api/settings, /api/admin/system/license, /api/admin/system/extension, /api/admin/finance/exchange/provider/active, /api/geo and /api/admin/system/geo-restriction. So you can always sign in and reach the licence screen, and nothing else will work until you do.

Go to /admin/system/license and enter your Envato purchase code. The screen is reachable from an Extension or Exchange detail page as well; it takes a productId query parameter to activate an addon rather than the core product.

Activates the core or an addon licence

If the server cannot make outbound calls, download the licence file from Envato, drop it into the lic/ directory at the project root and use the file-based activation on the same screen.

Activates from an Envato licence file already in /lic

Activation writes lic/<productId>.lic. That file is encrypted and bound to the machine, so it does not survive being copied to a different server — reactivate there instead. Back it up with .env, and note that lic/ is excluded from version control.

The platform re-checks its licence against https://updates.mashdiv.com on a heartbeat — hourly by default, and never more often than every five minutes. If that host is unreachable the install keeps working for a 72-hour grace period and then starts refusing. A firewall that blocks egress produces a site that worked fine for three days and then stopped.

The platform health screen

Once you are signed in, /admin carries a Platform Health card that probes eleven services for real — database, Redis, scheduler, email, exchange provider, the transaction, withdrawal, KYC and support queues, error rate and the wallet service — and scores them. The scheduler is broken out on its own line above the list, because it is the one failure the rest of the page cannot show you: when the cron process dies, every other probe stays green while nothing scheduled runs.

Use it as the day-two check. The shell commands above are for the first boot and for the moments when you cannot sign in at all.

It installed but nothing works

The maintenance server is up and the three real apps are not. pnpm stop puts it there on purpose; an interrupted update leaves it there by accident.

Check with pm2 list — you will see a maintenance app and no backend. pnpm start removes it and brings the platform back. If pnpm stop itself exited 1 earlier, it found a port still listening or a backend process running outside PM2, and refused to claim the platform was stopped; find and kill that process before starting again.

Almost always a missing location /api block in the reverse proxy. In production Next.js does not forward /api to the backend — those rewrites are development-only. Without a proxy rule, every API call hits Next, which returns its own 404 page, and the frontend reports errors it cannot explain.

The same block must carry WebSocket upgrades, because every realtime endpoint lives under /api too. Proxy /api to 127.0.0.1:4000 and / to 127.0.0.1:3000.

You are serving the site over plain HTTP. When NODE_ENV=production the backend issues its session cookies with Secure and SameSite=None. Browsers discard Secure cookies delivered over HTTP, and SameSite=None is invalid without Secure — so the login call succeeds, no cookie is stored, and the next request is anonymous.

HTTPS is not optional for a production install. Nothing in the product automates certificates; issue them yourself and terminate TLS at the proxy. The backend itself speaks plain HTTP and always has.

The production CORS allowlist is derived entirely from NEXT_PUBLIC_SITE_URL — it expands to the http/https and www/non-www variants of that one value. If the variable is unset, the allowlist is empty and every cross-origin call is refused.

Set it to the exact public URL, then rebuild the frontend. NEXT_PUBLIC_* values are inlined into the browser bundle at build time and the hostname is baked into the image allowlist, so changing the domain without pnpm build:frontend leaves the browser calling the old origin and rejecting images on the new one.

Exit 78 is EX_CONFIG and means one of exactly two things, both printed as a boxed message at the top of pm2 logs backend:

The Node version is wrong. The platform runs on Node 22, 24 or 26 and nothing else, because the pinned uWebSockets.js build ships prebuilt binaries only for those ABIs. On Node 20 the failure otherwise reads as a corrupt install — a missing .node file four frames deep. Check with node -v.

Or Redis is unreachable. See the Redis step above.

Both are stopped rather than restarted on purpose, so the explanation stays on screen instead of scrolling away behind sixteen restarts.

Something outside the platform is restarting it on a timer. From inside pm2 logs this is indistinguishable from a crash loop except that every exit code is 0 and nothing ever fails.

The usual culprit is a leftover systemd unit wrapping pnpm start with Restart=always: that command hands three apps to the PM2 daemon and correctly exits within seconds, systemd reads the exit as a death, and ten seconds later it restarts everything. The backend never lives long enough to finish booting, port 4000 never opens, and the frontend serves connection-refused with a log full of successful startup messages.

The backend detects this and prints a warning naming the suspects. Remove the unit — systemctl disable --now bicrypto then delete /etc/systemd/system/bicrypto.service — and check crontab -l for a @reboot or interval entry doing the same thing.

No withdrawals settle, prices do not refresh, investments never mature. The web tier runs with CRON_MODE=off, so the separate cron process owns every scheduled job — and when it is not running, nothing about the rest of the deployment looks wrong.

pm2 list will show cron missing or errored. pm2 logs cron says why. pnpm start brings it back and reconciles the layout so you do not end up with two schedulers instead.

The installer runs pm2 startup but never pm2 save, so PM2's boot hook restores an empty list. Start the platform, confirm pm2 list, then run pm2 save.

The installer's permission step runs find . -type f -exec chmod 644 across the whole tree, which strips the execute bit from every binary in node_modules/.bin. Only *.sh files are restored afterwards.

Reinstalling dependencies restores them. Do not reach for bash installer.sh --fix-permissions — it performs the same 644 sweep, and it also deletes frontend/.next, so the frontend will not start again until you run pnpm build:frontend.

Correct — there is not. The closing summary printed by the installer is stale; the same run deletes that unit deliberately. Use pnpm start, pnpm stop and pnpm restart from the project root.

Before you announce the site

A short list of things that are easy to leave undone and expensive to discover later.

Or delete the account entirely once your own Super Admin exists. It is published in the installer output.

Otherwise the next reboot brings back a machine with nothing listening.

The backend binds them without TLS and without a proxy in front. Only the proxy should reach them.

Nothing needs setting for a proxy on this machine — the backend honours a forwarding header from a loopback connection automatically. What it cannot do is invent one, so confirm the proxy sends it: Apache needs a2enmod headers and RequestHeader unset X-Forwarded-For; nginx needs proxy_set_header X-Forwarded-For $remote_addr;. Without it every visitor shares one rate-limit bucket and geo rules evaluate everyone as 127.0.0.1. Only a proxy on a different host needs TRUST_PROXY_CIDRS.

It holds the wallet encryption key and passphrase. Lose them and every custodial private key on the install is unrecoverable. No backup tool in the product covers this file.

/admin scores eleven probes plus the scheduler. Look at it once before you send anyone a link.