Ritzy One • Administrator / Vendor Guide — Internal

Companies on Their Own Subdomains

Run several unrelated companies — customers, or prospects on a demo server — on one Ritzy One install, each signing in at its own address such as acme.demo.ritzy.net.au. Each address shows and accepts only its own company, so nobody sees the names of the others.

Setting: LOGIN_COMPANY_SCOPE=host  |  Off by default  |  Internal / vendor tooling — not for customer distribution

1 What it does

A Ritzy One install keeps all its companies in one PostgreSQL database. Row-Level Security already keeps each company's data apart. But by default the sign-in page lists every company on the install to anyone who opens it, so two prospects sharing a demo server would see each other's company names.

With LOGIN_COMPANY_SCOPE=host the address in the browser decides the company:

It applies to the web app and all the phone apps (they share the same sign-in), the tenant portal's contract-number sign-in, and the public /api/auth/diag connection check. Tenant portal invitation links are not affected: the link itself names the account.

acme.demo.ritzy.net.au→nginx passes the address on→API reads “acme”→company ACME only

2 How an address picks its company

The company is the first part of the host name, matched to the company code without regard to case. LOGIN_HOST_MAP overrides that for any address you list.

Address openedCompanyWhy
acme.demo.ritzy.net.auACMEFirst part of the name
psk.ritzy.lan:8081 (a phone app)PSKThe port does not matter
demo2.demo.ritzy.net.auDEMO_2Listed in LOGIN_HOST_MAP
192.168.1.214none — or as mapped“192” is not a company code
www.demo.ritzy.net.aunoneNo company called WWW
Company codes and addresses
Addresses can only hold letters, digits and hyphens. A code such as DEMO_2 can never be matched from the first part of a name, so map it: LOGIN_HOST_MAP=demo2.demo.ritzy.net.au=DEMO_2. When choosing codes for a shared install, letters and digits only keep things simple.

3 When to use it, and when not

SituationUse
A demo server: several prospects, each looking at a sample or imported company for a few daysThis setting — one install, one subdomain each
A pilot: a prospect working on their real data for weeksA separate stack per customer (docker/new-stack.sh, see the Docker Installation guide). It has its own database, backups and upgrade timing.
A customer with several companies of their ownLeave the setting off. Their staff should see all their companies at one address.
Even simpler for a first demo
Import the prospect's company on your own machine and show it over a screen share. Their data never reaches a public server, and you can delete it straight afterwards.

4 Step 1 — Add the companies

In the DBA Console, add each company to the install:

Write down each company's code. It becomes that company's subdomain:

Company codeAddress
PSKpsk.demo.ritzy.net.au
ACMEacme.demo.ritzy.net.au

Give each prospect their own user in their own company. Do not give them DBA rights (see section 11).

5 Step 2 — Point the names at the server

On the internet

At your DNS provider, add one wildcard record. It covers every company, now and later, with no further DNS work:

*.demo.ritzy.net.au   A   <the server's public IP>

If the domain is on Cloudflare, follow section 5a instead: there's a choice to make there that affects the phone apps and HTTPS.

On your own network, for testing

No DNS is needed. On each PC you test from, open Notepad as Administrator, edit C:\Windows\System32\drivers\etc\hosts and add a line per company:

192.168.1.214   psk.ritzy.lan
192.168.1.214   acme.ritzy.lan

Use the server's own address in place of 192.168.1.214.

5a If the domain's DNS is on Cloudflare

In the Cloudflare dashboard, open the domain (ritzy.net.au) and go to DNS → Records. First decide how the subdomains go through Cloudflare. That is the Proxy status switch on the record:

Option A — DNS only (grey cloud) ✅ recommended for a demo serverOption B — Proxied (orange cloud)
What Cloudflare doesOnly answers the name with the server's IP. Visitors connect to the server directly.Visitors connect to Cloudflare, which forwards to the server. It hides the server's IP and absorbs attacks.
Phone apps on ports 8081–8096Work as they areDo not work. Cloudflare only forwards a few ports (HTTP 80, 8080, 8880, 2052, 2082, 2086, 2095; HTTPS 443, 2053, 2083, 2087, 2096, 8443). The phone apps would need moving to those ports.
CertificateThe server gets its own (Caddy in docker/trial-proxy does it automatically, or win-acme)Cloudflare's free certificate covers *.ritzy.net.au but not *.demo.ritzy.net.au (two levels deep). Use one-level names such as acme.ritzy.net.au, or buy Cloudflare's Advanced Certificate Manager.

Option A — DNS only

  1. Add record: Type A, Name *.demo, IPv4 address = the server's public IP, Proxy status DNS only (grey cloud), TTL Auto. Save.
  2. That's all on the Cloudflare side. Do the certificate on the server (step 5). Caddy's automatic certificates work because visitors, and the certificate check, reach the server directly on port 80.
  3. Test from any PC: nslookup acme.demo.ritzy.net.au answers with the server's IP.

Option B — Proxied

  1. Add record: Type A, Name * for acme.ritzy.net.au-style names (or *.demo if you have Advanced Certificate Manager), IPv4 address = the server's public IP, Proxy status Proxied (orange cloud). Records you already have (www, docs, lic2…) keep working: a named record always wins over the wildcard.
  2. SSL/TLS → Overview: set encryption mode to Full (strict). Never Flexible: that sends passwords unencrypted between Cloudflare and the server.
  3. SSL/TLS → Origin Server → Create Certificate: keep the hostnames *.ritzy.net.au and ritzy.net.au (add *.demo.ritzy.net.au if you use that), 15 years. Save the certificate and private key as two files on the server, e.g. C:\nginx\conf\ssl\ritzy-origin.pem and ritzy-origin.key, and point nginx at them:
    listen 443 ssl;
    ssl_certificate     C:/nginx/conf/ssl/ritzy-origin.pem;
    ssl_certificate_key C:/nginx/conf/ssl/ritzy-origin.key;
    This certificate is only trusted by Cloudflare, which is all it needs to be: visitors see Cloudflare's certificate.
  4. SSL/TLS → Edge Certificates: turn on Always Use HTTPS.
  5. Caching → Cache Rules: add a rule URI Path starts with /api/ → Bypass cache, so no API answer is ever served from Cloudflare's cache.
  6. Move the phone apps to Cloudflare-supported HTTPS ports (2053, 2083, 2087, 2096, 8443), or don't use them through this address.
  7. Optional but worthwhile: on the server's firewall, allow ports 80/443 only from Cloudflare's IP ranges, so nobody can reach the server around Cloudflare.
Cloudflare passes the address on
Whichever option you choose, the address the visitor typed reaches nginx unchanged, so the company is still picked from it. No Cloudflare rule is needed for that.
If sign-in or the phone apps fail only through Cloudflare
Check Security → Events. Bot Fight Mode or an Under Attack setting can block the apps' API calls. Add a rule that skips those checks for /api/.

6 Step 3 — nginx

nginx must hand the address the visitor typed to the API. The standard Ritzy One config already does this in every location /api/ block. Check it is there:

location /api/ {
    proxy_pass http://127.0.0.1:3001;
    proxy_set_header Host $host;   # required: the API reads the company from this
    ...
}

Then add the subdomains to the server_name line of each server { } block in C:\nginx\conf\nginx.conf, the web app on port 80 and each phone app on its own port:

server_name 192.168.1.214 *.demo.ritzy.net.au *.ritzy.lan;

Take a copy of nginx.conf first. Then test the config and restart nginx in PowerShell, as Administrator:

cd C:\nginx
.\nginx.exe -t
Restart-Service nginx
Restart, don't reload
nginx runs as a Windows service, so nginx -s reload is refused (Access is denied). Use Restart-Service nginx.

The phone apps keep their ports, e.g. acme.demo.ritzy.net.au:8081. CORS_ORIGIN does not need changing: each page talks to the API through its own address.

7 Step 4 — Turn it on

The setting goes in the API's settings file, C:\RitzyOne\packages\api\.env, not a .env in the install folder's root. On installs from November 2026 there is a commented-out section for it at the end of the file.

LOGIN_COMPANY_SCOPE=host
# Any address that is not a company code, comma-separated (address=CODE):
LOGIN_HOST_MAP=192.168.1.214=PSK,localhost=PSK,demo2.demo.ritzy.net.au=DEMO_2

Then restart the API in PowerShell, as Administrator:

Restart-Service ritzy-api
Map the addresses people already use
Once the setting is on, an address that names no company shows an empty sign-in page. If staff open the server by its IP address (http://192.168.1.214) or by localhost, map those to a company in LOGIN_HOST_MAP, or they will be locked out.
SettingValuesDefault
LOGIN_COMPANY_SCOPEall — every company at every address
host — each address only its own company
all
LOGIN_HOST_MAPComma-separated address=CODE pairs. The address is the host name only: no http://, no port. Spaces and case do not matter.empty

8 Step 5 — HTTPS

Before prospects sign in over the internet, put the addresses on HTTPS. Passwords should not travel in plain text, and the phone apps can only be installed from an HTTPS address.

9 Check that it works

  1. Open http://acme.demo.ritzy.net.au. The company box shows only ACME, already selected. Sign in.
  2. Open http://psk.demo.ritzy.net.au. It shows only PSK.
  3. Open an address that names no company, e.g. http://www.demo.ritzy.net.au. The company list is empty.
  4. Open /api/auth/companies on each address, e.g. http://acme.demo.ritzy.net.au/api/auth/companies. It returns one company, or [].
  5. Check a phone app too, e.g. http://acme.demo.ritzy.net.au:8081.
  6. If staff use the IP address, check it still signs in to the company you mapped.

10 On a Docker stack

A Docker stack reads its settings from the stack's .env (beside its docker-compose.yml). Add the same two lines there and restart the stack:

LOGIN_COMPANY_SCOPE=host
LOGIN_HOST_MAP=demo2.demo.ritzy.net.au=DEMO_2
docker compose up -d

Caddy in docker/trial-proxy passes the address on by default. Point every prospect's subdomain at the one shared demo stack's port.

11 What it does not cover

12 Troubleshooting

What you seeLikely causeFix
Sign-in page has an empty company listThe address names no companyCheck the first part of the address matches a company code, or add it to LOGIN_HOST_MAP. Restart ritzy-api.
Every company is still listedSetting not readCheck it is in packages\api\.env, not commented out, spelled host; restart ritzy-api.
Every address shows the same company, or nonenginx is not passing the address onAdd proxy_set_header Host $host; to each location /api/; restart nginx.
Address does not open at allDNS or hosts fileCheck the wildcard record, or the hosts file line on that PC; ping acme.ritzy.lan should answer from the server.
Correct password but Invalid user ID or passwordSigning in to a company that address doesn't belong toUse that company's own address.
Web app works but phone apps don't, through CloudflareProxied record; Cloudflare doesn't forward ports 8081–8096Use a DNS-only record, or move the apps to Cloudflare's ports (5a).
Browser certificate error on acme.demo.ritzy.net.au through CloudflareCloudflare's free certificate doesn't cover two-level namesUse DNS only, one-level names (acme.ritzy.net.au), or Advanced Certificate Manager.
526 Invalid SSL certificate / 525 SSL handshake failed from CloudflareFull (strict) is on but the server has no valid certificate on 443Install the Cloudflare Origin Certificate in nginx (5a, option B).
Staff locked out after turning it onThey open the server by IP or localhostMap those addresses in LOGIN_HOST_MAP.

13 Turning it off

Put a # back in front of LOGIN_COMPANY_SCOPE=host (or set it to all) and run Restart-Service ritzy-api. Every address lists every company again. Nothing is stored in the database, so turning it on and off is safe at any time.