Ritzy One • Administrator / Vendor Guide — Internal
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.
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.
The API looks for the company in this order:
LOGIN_HOST_MAP in the API's .env.| Address opened | Company | Why |
|---|---|---|
acme.demo.ritzy.net.au | ACME | First part of the name |
psk.ritzy.lan/crm/ (a phone app) | PSK | The path does not matter |
trial-one.demo.ritzy.net.au | ACME | ACME's Url is https://trial-one.demo.ritzy.net.au |
demo2.demo.ritzy.net.au | DEMO_2 | Listed in LOGIN_HOST_MAP |
192.168.1.214 | none — or as mapped | “192” is not a company code |
www.demo.ritzy.net.au | none | No company called WWW |
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.| Situation | Use |
|---|---|
| A demo server: several prospects, each looking at a sample or imported company for a few days | This setting — one install, one subdomain each |
| A pilot: a prospect working on their real data for weeks | A 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 own | Leave the setting off. Their staff should see all their companies at one address. |
In the DBA Console, add each company to the install:
DB_<code>.FDB).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 code | Address |
|---|---|
PSK | psk.demo.ritzy.net.au |
ACME | acme.demo.ritzy.net.au |
Give each prospect their own user in their own company. Do not give them DBA rights (see section 11).
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.
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.
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 server | Option B — Proxied (orange cloud) | |
|---|---|---|
| What Cloudflare does | Only 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/) | Work | Work: they are on the same address and port as the web app |
| Certificate | The 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. |
A, Name *.demo, IPv4 address = the server's public IP, Proxy status DNS only (grey cloud), TTL Auto. Save.nslookup acme.demo.ritzy.net.au answers with the server's IP.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.*.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.
/api/ → Bypass cache, so no API answer is ever served from Cloudflare's cache.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./api/.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
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.
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.
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
http://192.168.1.214) or by localhost, map those to a company in LOGIN_HOST_MAP, or they will be locked out.| Setting | Values | Default |
|---|---|---|
LOGIN_COMPANY_SCOPE | all — every company at every addresshost — each address only its own company | all |
LOGIN_HOST_MAP | Optional; 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 |
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.
docker/trial-proxy (Caddy) gets and renews a certificate for each subdomain by itself. It needs ports 80 and 443 free and the wildcard DNS record from step 2.*.demo.ritzy.net.au (a wildcard needs DNS validation at your DNS provider), then add listen 443 ssl; with the certificate files to each server block.http://acme.demo.ritzy.net.au. The company box shows only ACME, already selected. Sign in.http://psk.demo.ritzy.net.au. It shows only PSK.http://www.demo.ritzy.net.au. The company list is empty./api/auth/companies on each address, e.g. http://acme.demo.ritzy.net.au/api/auth/companies. It returns one company, or [].http://acme.demo.ritzy.net.au/track/.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.
| What you see | Likely cause | Fix |
|---|---|---|
| Sign-in page has an empty company list | The address names no company | Set 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 listed | Setting not read | Check it is in packages\api\.env, not commented out, spelled host; restart ritzy-api. |
| Every address shows the same company, or none | nginx is not passing the address on | Add proxy_set_header Host $host; to each location /api/; restart nginx. |
| Address does not open at all | DNS or hosts file | Check 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 password | Signing in to a company that address doesn't belong to | Use that company's own address. |
| Web app works but phone apps don't, through Cloudflare | The phone was given an old port address (:8081…): the apps have no ports now | Install 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 Cloudflare | Cloudflare's free certificate doesn't cover two-level names | Use DNS only, one-level names (acme.ritzy.net.au), or Advanced Certificate Manager. |
| 526 Invalid SSL certificate / 525 SSL handshake failed from Cloudflare | Full (strict) is on but the server has no valid certificate on 443 | Install the Cloudflare Origin Certificate in nginx (5a, option B). |
| Staff locked out after turning it on | They open the server by IP or localhost | Map those addresses in LOGIN_HOST_MAP. |
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.