Ritzy One • Installation & Licensing Runbook — Internal
The complete order of operations for standing up Ritzy One on a new server and licensing it — from the application install (which brings PostgreSQL with it), through creating the first company, to activating the licence with a single code. Written for Ritzy staff performing an installation; it is not a customer document.
Gather these before touching the server. Stopping half-way to hunt for one of them is the usual cause of a long install.
| Item | Why |
|---|---|
Both deploy bundles — RitzyOne-deploy.zip and (first time only) RitzyLicence-deploy.zip | Built on the dev machine to C:\Projects\ by package-deploy.ps1 and packages\license-server\package.cmd. |
| Administrator access on the server | Every installer writes services and firewall rules. |
| Internet access from the server or the EDB PostgreSQL 18 installer (Windows x64) copied over | The installer downloads PostgreSQL 18 (about 400 MB) when it is not installed; offline, pass -PgInstaller <file>. |
| The customer's seat count and purchased modules | Needed to mint the licence. |
| Today's support password | Gates first-run setup and every DBA Console action. It is date-derived and recomputed daily — not stored anywhere. |
| The company code (1–5 chars, A–Z/0–9), name, financial year and the administrator login to create | Entered in the first-run wizard. |
-SkipNodeInstall to manage it yourself.
The License Server is vendor infrastructure. It holds the RSA private signing key and must never be installed on a customer's box.
| Model | Where the License Server runs | When to use it |
|---|---|---|
| Hosted / SaaS (normal) | One Ritzy-controlled server on the internet, https://lic2.ritzy.net, shared by every customer. A new install uses it by default | Default. Gives you live suspend/revoke and automatic renewal delivery. |
| On-prem, connected | Same shared Ritzy server; the customer's site reaches it outbound over HTTPS | Customer hosts the ERP but has internet access. Same benefits as SaaS. |
| Air-gapped | Not reachable | No heartbeat, no activation code. Use the manual key path in §9; the licence then only ends at expiry. |
| All-in-one (lab/demo only) | Same box as Ritzy One, on http://localhost:4100 | Testing. Never for a real customer — it puts the signing key on their hardware. |
data\license-private.pem on the License Server signs every licence Ritzy has ever issued. If it leaks, anyone can mint licences. If it is lost, every issued licence stops verifying and every customer must be re-keyed. Back it up, and keep it off customer servers.
Skip this entire section if your License Server is already running — you only build it once and then share it across all customers. Jump to §4.
RitzyLicence-deploy.zip to the vendor server and unzip it to a permanent folder, e.g. C:\RitzyLicence. Do not run it from a temp folder — the licence database, its backups and the keys are created in data\ beside it. The licence database is Firebird: install the Firebird server (3, 4 or 5) on this machine first.install.cmd
| Prompt | Default | What to enter |
|---|---|---|
| HTTP port | 4100 | Keep 4100 unless it clashes. Must match your reverse proxy. |
| Bootstrap admin username | admin | Your vendor console login. |
| Password / Confirm password | — | A strong password. It is hashed on first boot; never leave it as changeme. |
| Public base URL | https://lic2.ritzy.net | The externally reachable URL. Accept the default on the live server; use http://localhost:<port> only for a local test. Used for PayPal returns and given to installs as the heartbeat URL. |
| Allowed console origin(s) | the public URL | Same HTTPS URL. Comma-separate if more than one. |
| Enable PayPal payments? | No | N unless you are taking card payments now; you can enable it later in .env. |
| Enquiry intake key | blank (intake off) | The shared key the website sends with each quotation request. Type generate for a random one (it is printed — copy it into the website's ritzy-config.php), or leave blank to keep the intake off. |
| Intake posts accepted per minute | 30 | Asked only when a key is set. Keep 30. |
| Is this server proxied by Cloudflare? | Yes | Y for lic2.ritzy.net. The client address is then read from CF-Connecting-IP, only for requests from Cloudflare's edge. |
The installer generates ADMIN_JWT_SECRET automatically, installs dependencies, registers and starts the Windows service ritzy-license (through NSSM, like Ritzy One's ritzy-api), opens the firewall port and runs a health check. Run install.cmd and deploy.cmd as Administrator.
logs\service.log / logs\service-error.log. Older installs ran the License Server under PM2 with a boot hook that only fired when the installing user signed in, so after an unattended restart it stayed down while Ritzy One came up. On such a server run deploy.cmd as Administrator, as the same Windows user that installed it: it removes the PM2 ritzy-license process and moves the app to the service.
update.cmd there and choose Run as administrator. It finds the folder the service runs from, replaces the compiled files, keeps data\ and .env, and restarts the service. Ritzy One has its own update.cmd that works the same way.
Browse to the console (the installer prints the URL) and sign in with the bootstrap admin.
BOOTSTRAP_ADMIN_PASSWORD in .env afterwards does nothing; it is only read when the database is empty.canSign=false).data\ folder (see §12).Customers post their licence tokens to this server, so it must not be plain HTTP in production. A ready nginx config ships in the bundle as nginx.sample.conf:
licenses.example.com in three places (the two server_name lines and the certificate paths)./etc/nginx/sites-available/ritzy-license.conf, symlink into sites-enabled.sudo certbot --nginx -d licence.yourdomain, then sudo nginx -t && sudo systemctl reload nginx.PUBLIC_BASE_URL and CORS_ORIGIN in .env to the HTTPS URL and restart: Restart-Service ritzy-license (elevated PowerShell).It proxies to http://127.0.0.1:4100. On Windows, mirror the same thing with IIS ARR + URL Rewrite.
sub_filter or any body rewriting to /api/webhooks/*. PayPal's signature is computed over the raw body; altering it makes every webhook fail verification.
healthcheck.cmd https://licence.yourdomain
Exit code 0 = healthy. It prints the running version and canSign, and checks the Windows service ritzy-license is running. With no argument it tests http://localhost:4100. It is a drop-in for a Windows Scheduled Task.
Ritzy One keeps every company in one PostgreSQL database, kept apart by Row-Level Security. Nothing has to be installed beforehand: install.ps1 installs PostgreSQL 18 with the EDB installer in silent mode (server and command-line tools only), puts its data folder under the base folder (<RITZY_ROOT>\PostgreSQL\18\data), sets the postgresql-x64-18 service to start automatically, generates two passwords and writes them into packages\api\.env as PG_URL (the postgres superuser) and PG_APP_URL (ritzy_app). On its first start the app creates the database, the schema and the ritzy_app login itself.
postgres password once — it asks, or pass -PgSuperPassword on an unattended run.
An older PostgreSQL (17 or before) is not reused and 18 is not installed beside it: the installer stops and lists what it found. Uninstall it if nothing in it is needed, or move its data to 18 first (dump and restore, or pg_upgrade; see DEPLOYMENT.md, Upgrading from PostgreSQL 17), then run the installer again.
.FDB files, which the server no longer reads. Convert them into PostgreSQL before running the first-run wizard: npm run pg:convert from a RitzyWeb source checkout on a machine with Firebird (DEPLOYMENT.md, Moving Firebird data into PostgreSQL). The installer warns when it finds DB_*.FDB files in <RITZY_ROOT>\Data and PostgreSQL has no company yet.
Getting this wrong is the single most common installation mistake. RITZY_ROOT is the base folder — not the Ritzy folder inside it. Default C:\Ritzy Solutions:
| Path | Holds |
|---|---|
<RITZY_ROOT>\Ritzy\System | Install-wide state kept outside the database (licence revocations, billing) and the nightly backup script |
<RITZY_ROOT>\PostgreSQL\18\data | The PostgreSQL data folder — every company |
<RITZY_ROOT>\Backups | DBA Console backups, and Backups\PostgreSQL for the nightly pg_dump |
So for the default, RITZY_ROOT=C:\Ritzy Solutions — never C:\Ritzy Solutions\Ritzy.
The installer opens these inbound TCP rules. Confirm nothing else is using them:
| Port | Used by |
|---|---|
| 80 | Dashboard (nginx) |
| 3001 | API |
| 8081 | Track Shipment PWA |
| 8082 | Customer Statement / Dues PWA |
RitzyOne-deploy.zip to the install folder, e.g. C:\RitzyOne.powershell -ExecutionPolicy Bypass -File install.ps1
| Prompt | Answer |
|---|---|
| Enter the Ritzy base folder path | The RITZY_ROOT base folder — see §4.2. Only asked if it can't be auto-detected. |
| Password of the PostgreSQL superuser 'postgres' | Only asked when PostgreSQL 18 was already installed. A new install generates it. |
Overwrite existing nginx.conf? | Y on a new server |
.env at packages\api\.env with PG_URL / PG_APP_URL and a random JWT_SECRET.ritzy-api plus Windows services ritzy-api and nginx (via NSSM) so it survives reboot.pg_dump -Fc at 01:30 into <RITZY_ROOT>\Backups\PostgreSQL, 14 days kept..env is never touchedpackages\api\.env already exists the installer keeps it. It only adds PG_URL / PG_APP_URL when they are missing and comments out old FB_* lines; a PG_URL already there (even one pointing at another server) is used as it is. When re-installing over an old build, check that file yourself for anything new.
install-app.ps1 instead — it installs, builds and restarts, with no prompts and no .env changes; the app applies any database upgrades itself when it starts. The usual update is simply: extract the new zip over the folder, then pm2 restart ritzy-api.
Confirm the API is live and running the build you expect:
curl http://localhost:3001/api/version
On a server with no companies yet, the login page replaces the sign-in form with a Set up Ritzy button.
http://<server>/ and click Set up Ritzy.| Field | Notes |
|---|---|
| Company code | 1–5 characters, A–Z and 0–9 only. Identifies the company in the database and cannot be changed later. |
| Company name | Stored upper-case. |
| Base currency | Drives the tax/registration caption (AUD→ABN, BHD→CR.No., SAR→VAT No., AED→TRN). |
| Financial year | Start/end month and year. Must span 1–24 periods; one PERIODS row is created per month. |
| Administrator login | The customer-facing admin account. DBA is reserved and will be rejected — use ADMIN. |
DBA support account (password t@11b0y) so the DBA Console is reachable immediately. It is the same on every install. Sign in as DBA, go to Tools → Change Password, and set a unique one before the server leaves your hands. Never give DBA credentials to the customer — they use ADMIN.
https://lic2.ritzy.net unless another URL is set): with its contact details (email, telephone, city, country, base currency, modules, server name), under the licence's customer when the install is licensed (re-created from those details if that customer is no longer on file), or under a customer the License Server creates from them when it is not. The creation log says which. Saving Settings → Company later sends the details again, so the License Server's copy stays current.
Licensing is now a single field. You mint a short code on the License Server and the DBA pastes it into the install.
ACME.DBA account is seat-exempt so support can always get in. Avoid 1-seat tokens outside a lab — they lock ordinary users to a single browser.
| Field | Guidance |
|---|---|
| Times it can be used | 1 for a single install. Use 0 (unlimited) only when a customer is rebuilding servers, and revoke it afterwards. |
| Expires | Default 30 days. Leave empty for no expiry. |
| Note | Internal, e.g. “emailed to their IT on 26 Jul”. |
RTZY-4K7M-9XQP-2WDF.
Any administrator can also do this without the DBA Console or the support password: click Activate beside the licence message at the top of the screen and paste the code (after the trial ends an administrator's sign-in opens straight onto an Activate Ritzy screen). That path uses the License Server the install is already configured for. The DBA Console route below is the one to use when the server URL has to be set.
DBA and open Tools → DBA Console → Licensing.https://lic2.ritzy.net unless the install was pointed at another License Server; only when the URL was deliberately cleared does a second field appear for it (it is remembered afterwards).That one action does all of this:
Hard-reload the browser (the badge caches per page load). The header badge should read a green Licensed; hover it for plan, seats and expiry. The Licence on <company> card shows the customer, seats, modules and validity.
Walk this before you call the installation done.
/api/version returns the build you deployedritzy-api and nginx are Running and set to AutomaticADMIN can sign inDBA password has been changed from the default.dump is in <RITZY_ROOT>\Backups\PostgreSQL; that folder is copied off the serverADMIN credentials — and not the DBA onesWhen the install genuinely cannot reach the License Server, use the manual path under Advanced / offline setup on the Licensing tab. The order matters here — this is exactly what the activation code exists to avoid getting wrong.
.lic. A key names its company and installs into that company's database only; issue one per company.An air-gapped licence cannot be suspended or renewed remotely — it simply ends at validTo plus grace. Re-key by repeating step 3 with a fresh .lic.
packages\api\.envpackages\api\.env, i.e. C:\RitzyOne\packages\api\.env — not a .env in the deploy root. Editing the wrong one is a classic hour-waster. Any change here needs pm2 restart ritzy-api.
| Variable | Default | Purpose |
|---|---|---|
RITZY_ROOT | C:/Ritzy Solutions | Base folder — see §4.2 |
PG_URL | generated | The postgres superuser: the app makes the database ready from it on every start. Also read by the nightly backup |
PG_APP_URL | generated | The ritzy_app login every request uses (Row-Level Security applies to it) |
JWT_SECRET | generated | Mandatory. Changing it signs everyone out |
PORT | 3001 | API port |
LICENSING_ENABLED | unset | Leave unset so the console toggle works. Setting it to true or false pins enforcement and makes the toggle read-only |
LICENSE_HEARTBEAT_URL | unset | Leave unset — the install defaults to https://lic2.ritzy.net and Activate saves the server it used. An env value pins it and disables the UI field |
LICENSE_PUBLIC_KEY[_FILE] | unset | Leave unset — Activate stores the trusted key. An env value pins it |
LICENSE_HEARTBEAT_INTERVAL_MIN | 60 | Heartbeat period, minimum 5 |
LICENSE_SIGNING_KEY[_FILE] | — | Issuer only. Never set this on a customer install. |
ANTHROPIC_API_KEY | empty | Optional — enables the AI features (Paste Enquiry, CRM) |
.env| Variable | Default | Purpose |
|---|---|---|
PORT | 4100 | Serves both API and console |
ADMIN_JWT_SECRET | generated by installer | Mandatory. If unset, every restart signs all admins out |
BOOTSTRAP_ADMIN_USER / _PASSWORD | admin / changeme | Seeds the first admin — only on an empty database. Use reset-admin.cmd afterwards |
PUBLIC_BASE_URL | https://lic2.ritzy.net | Handed to installs as the heartbeat URL; base for PayPal returns |
CORS_ORIGIN | http://localhost:5180 | Must list the console's real URL |
LICENSE_SIGNING_KEY_FILE | ./data/license-private.pem | Written by Generate key pair |
LICENSE_BILLING_ENABLED | false | Set true for the daily auto-renew and dunning sweep |
PAYMENT_GATEWAY, PAYPAL_* | none | Optional online payments |
ENQUIRY_INTAKE_KEY | empty | Shared key for website enquiries (X-Enquiry-Key). Empty = intake off, the endpoint answers 404 |
ENQUIRY_INTAKE_PER_MINUTE | 30 | Intake posts accepted per minute from one address |
BEHIND_CLOUDFLARE | true | Read the client address from CF-Connecting-IP (Cloudflare edge ranges only) |
The token's signature doesn't match the public key this install trusts. Compare fingerprints — curl https://<license-server>/api/license/public-key against the one on the Licensing tab. If they differ, re-trust the key (or just use an activation code, which does it for you). If they match, the token was signed before a key-pair rotation — re-issue the customer's key.
A timezone bug in builds before license-tz-validity-2026-07-26: date-only bounds were compared against UTC midnight, so anywhere ahead of UTC (Australia, the Gulf) a licence starting today read as not-yet-valid all morning. Check /api/version and upgrade.
Deliberately uniform — it means unknown, expired, revoked or already used. Open the customer on the License Server and read the code's real status in the Activation code table, then issue a new one. (Single-use codes are the default, so a second attempt on the same code always fails.)
The code is valid but the customer has neither a subscription nor an issued key. Create the subscription first, then activate.
Brute-force throttling, 10 attempts per minute per IP. Wait a minute.
Renewals arrive on the hourly heartbeat, and on login when a licence would otherwise block. Check that the install has a heartbeat URL and enforcement on. If the subscription was renewed on the server but the install is offline, it stays on its old token until it can reach the server again.
reset-admin.cmd :: list admins
reset-admin.cmd admin "NewPassw0rd" :: reset a password
reset-admin.cmd admin "NewPassw0rd" --clear-2fa
It works with the service running.
The seat cap comes from the token when a licence is present. Raise seats on the subscription and save (it re-mints and delivers automatically), or clear stale sessions in DBA Console. DBA is always seat-exempt.
Usually it simply isn't in the token: edit the subscription, add the module, save — the new entitlement is delivered on the next heartbeat with no database change. The gated add-ons are Liner (LN), Marine (MA), Warehousing (WH), Inland Transport (TR), Shipchandling (SC), Payroll (E) and CRM (CR); Cargo (SP) and Accounts (AC) are core and always available.
VISION, and the side menu joins VISION before the licence filter runs — so the add-on can never show, whatever the token says. Run the module migrations once against that database from DBA Console → Upgrade: 008, 009, 010, 017, 018, 019, 020, 021, 022, 023, 026. They are idempotent, and they also grant the package rights, so sign out and back in afterwards. The give-away symptom is Payroll showing while every other add-on is missing — Payroll is the one gated module the old schema seeded by default.
The module name lives in VISION.MODULENAME and older databases seeded it as “Shipping”. Run 027_cargo_module_rename.sql from the Upgrade tab. (The product itself remains Ritzy Shipping; only the freight-forwarding module is called Cargo, to distinguish it from Liner, Marine and Inland Transport. “Shipping Line” stays as-is — that is the ocean carrier, not the module.)
Hard-reload the browser. A cached bundle regularly masquerades as a bug.
The licence database backs itself up: a gbak backup runs daily into data\backups\ (the newest 14 kept), and Settings → Database backup in the console backs up now, downloads, uploads and restores. A restore checks the backup first and takes a backup of the current database before replacing it, so it can be undone. Copy the .fbk files off the machine as well, with the rest of the data\ folder and .env:
| File | Why it matters |
|---|---|
data\license-private.pem | Irreplaceable. Lose it and every licence ever issued stops verifying |
data\backups\*.fbk | Customers, subscriptions, invoices, activation codes, audit trail — the gbak backups of data\LICENSE.FDB (copy the backups, not the live database file) |
data\license-public.pem | Derivable from the private key, but back it up anyway |
.env | ADMIN_JWT_SECRET and gateway credentials |
The nightly dumps in <RITZY_ROOT>\Backups\PostgreSQL\ (restore with pg_restore --clean --if-exists -U postgres -d ritzy <file>), plus <RITZY_ROOT>\Ritzy\System\ and packages\api\.env. The licence itself needs no special backup — re-activating restores it.
pm2 status :: what is running
pm2 restart ritzy-api :: after an .env change or update
pm2 logs ritzy-api --lines 100 :: recent API log
Restart-Service ritzy-license :: License Server (vendor box, elevated PowerShell)
healthcheck.cmd :: License Server health, exit 0 = OK
RitzyOne-deploy.zip over the install folder.pm2 restart ritzy-api./api/version shows the new build, and hard-reload the browser.To repeat an installation test from a clean slate. Neither uninstaller deletes data by default, so the steps below are what actually clears it.
lic2.ritzy.net or a customer server. Deleting the License Server's private signing key makes every licence ever issued unverifiable, and every customer install would need a new public key and new licences.
Every company lives in one PostgreSQL database (default name ritzy). Dropping it and restarting the app gives an empty install at the first-run company setup (§6). uninstall.ps1 does not touch the database.
pm2 stop ritzy-api (or net stop ritzy-api where it runs as a Windows service).postgres password in PG_URL in packages\api\.env (postgres://postgres:<password>@localhost:5432/ritzy), then drop the database — use the name from PG_URL if it isn't ritzy:
"C:\Program Files\PostgreSQL\18\bin\psql.exe" -h localhost -U postgres -d postgres -c "DROP DATABASE ritzy WITH (FORCE);"
WITH (FORCE) closes any connections still open.pm2 start ritzy-api (or net start ritzy-api). On start it uses PG_URL to create a new empty database, the schema and the ritzy_app login.Keep packages\api\.env — it holds the PostgreSQL logins the app needs to rebuild the database. Optional extras:
| To also clear | Do this |
|---|---|
| Nightly database dumps | Delete <RITZY_ROOT>\Backups\PostgreSQL\ |
| DBA console backups | Delete the files in <RITZY_ROOT>\Backups\ |
| Old Firebird companies | DB_*.FDB files in <RITZY_ROOT>\Data are not affected by the drop, and the installer will offer to convert them again. Move them away if the test should start with none |
| The whole install | After the steps above, run uninstall.ps1 as Administrator: it removes the services, firewall rules and the install folder (including .env, so note PG_URL first if you will reconnect to the same PostgreSQL server). It stops only ritzy-api and node processes running from the install folder, and leaves the PM2 daemon up while it runs anything else, so a License Server on the same machine keeps running |
From the License Server folder, in a Command Prompt as Administrator:
uninstall.cmd -Yes -PurgeData
This removes the Windows service ritzy-license (and any PM2 process or boot hook an older install left), the firewall rule, and deletes .env and the whole data\ folder: the licence database (LICENSE.FDB, and an old license.db if present), data\backups\ and the signing key pair. Run uninstall.cmd without flags to be asked about each step instead (data is kept unless you say yes).
-RemoveDeps to delete node_modules\, dist\ and client-dist\ as well — but not on an extracted deploy zip, which ships dist\ prebuilt and cannot rebuild it; re-extract the zip instead.LICENSE_FB_DATABASE pointed outside data\, delete that file yourself once the service is stopped.Then run install.cmd again (§3). The first start creates an empty licence database and the bootstrap admin, and you generate a new key pair in Settings.