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 API looks for the company in this order:

  1. The company whose Url is that address. Set it in the DBA Console: Companies → Edit → Url. This is the easiest way, and works for any address.
  2. An address listed in LOGIN_HOST_MAP in the API's .env.
  3. The first part of the host name, matched to the company code without regard to case.
Address openedCompanyWhy
acme.demo.ritzy.net.auACMEFirst part of the name
psk.ritzy.lan/crm/ (a phone app)PSKThe path does not matter
trial-one.demo.ritzy.net.auACMEACME's Url is https://trial-one.demo.ritzy.net.au
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 give that company a Url (or map it in LOGIN_HOST_MAP). 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:

Then click Edit on each company and enter its address in Url, e.g. acme.demo.ritzy.net.au (with or without https://). The Companies list shows each Url. Two companies cannot share one. If you leave Url empty, the company's code is used as its 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 under paths (acme.ritzy.net.au/crm/)WorkWork: they are on the same address and port as the web app
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. Give the phone apps their path addresses: https://acme.ritzy.net.au/track/, /statement/, /crm/ and so on (the list is in the Mobile Apps manual, section 1). Nothing else is needed: the paths are on port 443 with the web app and covered by the same certificate. Set them in Tools → PWA Apps so the QR codes point there.
  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, which also serves every phone app under its path:

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 are under paths on the same address, e.g. acme.demo.ritzy.net.au/track/. CORS_ORIGIN does not need changing: each page talks to the API through its own address, and the paths are the same origin as the web app.

Optional: send unknown addresses to your website

With a wildcard record, any name reaches the server, so anything.ritzy.net shows an empty sign-in page. To send such addresses to your website instead, add this to the port 80 server { } block, above location /, and the two lines marked inside location /:

# An address that names no company goes to the website. The API answers 204 or 403.
location = /_host_check {
    internal;
    proxy_pass http://127.0.0.1:3001/api/auth/host-check;
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header Host $host;
}
location @unknown_host {
    return 302 https://ritzy.net.au/;
}

location / {
    root C:/RitzyOne/packages/web/dist;
    auth_request /_host_check;          # add
    error_page 403 = @unknown_host;     # add
    ...
}

It needs a Ritzy One API from November 2026 or later (which has /api/auth/host-check). With LOGIN_COMPANY_SCOPE off every address passes, so it is harmless there. The phone apps' location /track/ … blocks in the same server can take the same two lines. Every page request asks the API first, so if the API is stopped the pages show 500 instead of loading.

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_MAPOptional; a company's Url in the DBA Console does the same and wins. Comma-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. If you added the optional redirect in step 3, an address that names no company opens your website instead of the sign-in page.
  3. Open http://psk.demo.ritzy.net.au. It shows only PSK.
  4. Open an address that names no company, e.g. http://www.demo.ritzy.net.au. The company list is empty.
  5. Open /api/auth/companies on each address, e.g. http://acme.demo.ritzy.net.au/api/auth/companies. It returns one company, or [].
  6. Check a phone app too, e.g. http://acme.demo.ritzy.net.au/track/.
  7. 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 companySet that company's Url to the address in the DBA Console (takes effect at once, no restart), or check the first part of the address matches a company code.
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 CloudflareThe phone was given an old port address (:8081…): the apps have no ports nowInstall the app again from its path address (/track/, /crm/…; its QR code is on Tools → PWA Apps).
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.