Ritzy One • Administrator / Vendor Guide — Internal
How Ritzy sells and controls Ritzy One by concurrent-user count and per-module add-ons — run entirely from the standalone Ritzy License Server: taking enquiries and writing proposals, issuing licence keys, remote heartbeat & revocation, subscription billing with auto-renew, and online payments via PayPal. This guide is for Ritzy staff, not end customers.
A customer's entitlement is a signed licence token — a small tamper-evident record (RSA-signed, JWT-style) that carries the concurrent-seat count, the entitled add-on modules, and the validity window. One token is installed per company database (in the WEB_LICENSE table).
Subscriptions and billing are per database. Each customer has an automatically numbered Company ID on the License Server. Under it, every company of their installation is a subscription of its own, keyed by Company ID + Company Code, with its own seats, modules, term, invoices and licence key. The key names its company, so it only works in that company's database, and an unpaid invoice suspends only the database it bills. Keys issued before this change carry no company code and still cover the whole installation until Ritzy assigns the subscription a code.
New companies register themselves. Every time an installation creates a company (New Company wizard, login-page Company Setup, or DBA Console), it reports the new code and name to the License Server. When no License Server URL is set, it reports to https://lic2.ritzy.net.
The License Server keeps each company's setup details, not just its code and name: the customer's page lists them in a Registered companies card (code, name, email, telephone, location, base currency, modules, server, when it registered and when its details were last updated). Saving Settings → Company in Ritzy One sends the stored details again (after the save, so an unreachable License Server never holds it up), so the card stays current; a blank field never clears a value already held.
Nothing is billed or licensed until Ritzy adds a subscription. Companies created before the install had a licence are reported again under the customer when it is activated. If the License Server cannot be reached, or is an older version that refuses an unlicensed install, the creation log says so — the company is still created, and Ritzy adds it by hand.
Two keys, two roles:
The running app enforces the token in three places: the concurrent-seat cap at login, module access (add-on routers & the sidebar menu), and expiry (login is blocked past the validity window plus grace).
What counts as a seat. Seats are counted per browser device signing in to the web app; all tabs of one browser count once. Two kinds of sign-in are exempt and consume nothing: the DBA account (so a mis-sized licence can never lock you out of the console that fixes it) and every phone app registered in Tools → PWA Apps — Track, Dues, CRM, Delivery, Notify, Ask and Support, plus any the customer adds themselves. Phone apps are companions to a desk seat, not seats of their own. Module entitlement and expiry do still apply to them: a phone app cannot reach a module the customer has not bought, and an expired subscription blocks its login like any other.
DBA and any administrator (ADMINISTRATOR group) can still sign in: the DBA as usual, an administrator into a restricted session that shows only the activation screen. A red banner and a Trial badge show when the trial ends. Activating a licence ends the trial at once. Installs that already run with enforcement on but no token start their 10 days when they are upgraded to this build.Issue mint & deliver signed keys Enforce remote heartbeat & revocation Bill subscriptions, auto-renew & PayPal
Licensing is run from a separate, vendor-only application — the Ritzy License Server (package license-server). You run it centrally; customers never see it. It is self-contained (its own Firebird database, LICENSE.FDB; see §16) and has its own login with optional two-factor authentication.
It is the only place the private signing key lives, so licence keys can only be minted here. A customer's own copy of Ritzy One can verify and install a key, but contains no signing path at all.
| Screen | What you do there |
|---|---|
| Dashboard | At-a-glance totals — customers, active & suspended subscriptions, open invoices, licensed seats, upcoming renewals, and the signing-key fingerprint. The Ritzy Licensing phone app card holds the phone app's install QR code (below). |
| Enquiries | The sales inbox: website quotation requests and leads typed in from the phone or email. The menu item shows a badge with the number of new ones. See §9. |
| Proposals | The price calculator and numbered proposals (RQ-YYYY-NNNN) with an AI-drafted cover letter, module features and an implementation timeline; revisions, print preview, and the hand-off to a subscription once accepted. Called Quotations before October 2026. See §10. |
| Customers | The customer list. Open one to manage its subscription, invoices, issued keys (including a one-off Issue stand-alone key), saved payment method, and to suspend / revoke / re-activate it. Its Proposals tab lists that customer's proposals. |
| Revocations | The remote kill-switch list — suspend or revoke a customer by id, or re-activate. |
| Settings | Generate / rotate the signing key pair, view & copy the public key, run the renewal sweep, configure the PayPal gateway, edit the Pricing rate card, change your password, and enrol two-factor. |
Ritzy Licensing is an installable phone app for the same server, at https://<server>/app/ (for the live server, https://lic2.ritzy.net/app/). It uses the same login, two-factor, roles and permissions as the console — there is no extra port, service or installer step. Each tab and button appears only for a role that holds its permission, and the server checks every call as usual.
PUBLIC_BASE_URL when that is an https:// address, so set it (§13). Until it is, the card shows a warning.| Tab | What you do there |
|---|---|
| Home | What needs someone now: customers waiting in live chat, open and urgent tickets, new enquiries, overdue invoices, suspended databases, trials ending this week, and installs with no heartbeat for 3 days or more. Key figures and quick actions. |
| Customers | Search, then open a customer: its databases, suspend / revoke / re-activate (the whole customer or one database), auto-renew, share a licence key, activation codes (create, share, revoke), keys issued, invoices and install check-ins. |
| Enquiries | Status tabs; call, WhatsApp or email the lead (which marks it contacted); status and notes; a new enquiry with AI Paste. See §9. |
| Support | The live-chat queue (join, reply, hand back, close), the needs us inbox, and escalated tickets with photos, replies and internal notes. See §14. |
| More | Invoices (payment link through the phone's share sheet, mark paid, charge the saved method, PDF, void), issue a trial, activation code, check a licence key, the suspended & revoked list, install check-ins, run billing, and your account: set up two-factor straight into the phone's authenticator, change password, and a QR to install on another phone. |
Proposals, the rate card, users, the knowledge base and server settings stay in the desk console.
On a customer install, the app's DBA Console → Licensing tab is install-only: paste the key (or upload the .lic file) Ritzy supplied and install it, view what is installed, or remove it. There is deliberately no key-issuing, key-pair, subscription or revocation tooling on a customer install.
BOOTSTRAP_ADMIN_USER / BOOTSTRAP_ADMIN_PASSWORD). Change that password immediately and enable two-factor — both in Settings..env.example to .env and set at least ADMIN_JWT_SECRET and a strong BOOTSTRAP_ADMIN_PASSWORD; then build and start it. It listens on PORT (default 4100) and serves both the API and the admin console.https://lic2.ritzy.net out of the box (step 6), so normally you just click Auto-configure from License Server — one click, it fetches and trusts the key from that server. Otherwise enter the License Server URL (https://lic2.ritzy.net, or http://localhost:4100 for a lab box running both) and click Fetch & trust, or paste the public key PEM. It takes effect immediately, with no .env edit or restart. (Alternatively, set LICENSE_PUBLIC_KEY_FILE or LICENSE_PUBLIC_KEY in that install's .env; an env value takes precedence and pins the key.) The public-key fingerprint shown in Settings and on the Licensing tab lets you confirm a match.https://lic2.ritzy.net, and creating a company database records that address in DBA Console → Licensing → License Server URL. To use a different server, enter its base URL there (e.g. http://localhost:4100 on a lab box); the /api/license/heartbeat path is added for you and it takes effect with no restart. A URL already saved there is never replaced. This drives the suspension/renewal heartbeat and the one-click Auto-configure from License Server button in step 4. (Equivalent env var: LICENSE_HEARTBEAT_URL, which pins it if set. For pure offline / on-prem, clear the URL in the console.).env edit or restart, and activating a code (see §4) turns it on for you. (Equivalent env var: LICENSING_ENABLED, which pins enforcement either way when set and makes the switch read-only.) Once enforced, a licence badge appears in the app header — green Licensed when a valid key is active, amber in the grace period, and red (Trial / Expired / Suspended / Unlicensed) otherwise; hover it for the plan, seats and expiry. With enforcement off it shows a neutral grey Unlicensed (the app runs unrestricted)..env value if you pinned it there).In the License Server console:
saas / onprem / hybrid, where hybrid is e.g. their own server plus Ritzy-hosted apps) is informational only: it is shown on the customer and has no effect on licensing, heartbeat or billing.RTZY-XXXX-XXXX-XXXX — is shown once and copied to your clipboard. That single string is all the customer needs; no key file and no public key.DBA: Tools → DBA Console → Licensing, paste the code into Activate this installation and click Activate. The install then trusts the vendor's public key, pulls and verifies its licence, saves the heartbeat URL, installs into every company, and switches enforcement on — in one step, so the trust-then-install ordering cannot be got wrong. The License Server URL field is needed only if the install does not already have one.ADMINISTRATOR group clicks Activate on the licence banner at the top of the screen and pastes the code. After the trial has ended an administrator can still sign in — they see only an Activate Ritzy screen, then sign in again once it succeeds. This path always activates against the License Server the install is already configured for (it has no URL field); an install that must be pointed at a different server is activated by the DBA in the DBA Console.When the install cannot reach the License Server, fall back to the old path: Copy key or Download .lic, and have the customer use Advanced / offline setup on the Licensing tab — first Trusted public key (paste the PEM), then Install a key by hand. The key is verified against the trusted public key before it is written; an unverifiable key is refused. If the install does know a License Server URL, installing an unverifiable key now auto-fetches that server's public key and retries once rather than simply failing.
| Field | Meaning |
|---|---|
| Customer code / id | Stable identifier used by billing and revocation (e.g. ACME). Applies across all of that customer's installs. |
| Concurrent seats | Maximum simultaneous devices (browsers). Replaces the legacy hardware-key cap when a token is present. |
| Modules | The paid add-ons: Cargo, Liner, Marine, Warehousing, Transport, Shipchandling, Payroll, CRM, Real Estate. Anything omitted is hidden and blocked. |
| Valid to / Grace | The term end, plus a grace period after which login is blocked. |
.lic from there.Seats are a single global pool of concurrent devices. All tabs/windows of one browser count once; the Customer Statement PWA is exempt and never consumes a seat.
Modules are sold as per-module add-ons. Enforcement separates “installed” from “entitled”: a company database physically contains every module's tables, but if the module isn't in the token it is hidden from the menu and its routes return “not included in your subscription”. Selling a module later is just re-issuing the token with one more entry — no database change.
A newly granted module installs itself. When a key that adds a module reaches the customer — activating a code, installing a key, or a plan change delivered by the heartbeat — the install sets that module up in the company database (its menu entry, its tables and its admin rights) so it appears straight away. Only modules the new key grants and the previous one did not are installed this way; a module that was already licensed but skipped in the New Company wizard is left alone and is listed in the result, pointing at DBA Console → Modules.
VISION table, and the side menu joins on VISION before the licence filter runs — so an add-on will stay invisible there however the token is issued. Fix it once per database from DBA Console → Upgrade by running the module migrations (008, 009, 010, 017–023, 026); they are idempotent. The tell-tale is Payroll appearing while Liner, Marine, Warehousing, Inland Transport, Shipchandling and CRM do not.
Each licensed install periodically checks in with the License Server and applies the answer — so you can cut off or restore access, and deliver renewed tokens, between renewals.
Installs heartbeat to https://lic2.ritzy.net/api/license/heartbeat by default. To use another server, set its URL in DBA Console → Licensing → License Server URL (or pin it with LICENSE_HEARTBEAT_URL). For offline / on-prem installs, clear the URL there (revocation then only takes effect at token expiry).
A subscription turns a licence into a recurring plan that renews itself and chases non-payment. It is managed on a customer in the License Server console.
Open a customer → Create subscription and set seats, modules, currency / amount, billing cycle (Monthly / Quarterly / Half-yearly / Yearly), Days, and the three day windows below. Saving mints the first token and raises the first invoice.
Days sits between Start date and Next renewal. Picking a cycle fills it with that cycle's default — Monthly 30, Quarterly 90, Half-yearly 182, Yearly 365 — and Next renewal is the start date plus Days, recalculated as either changes. Change Days for a one-off length (a 45-day term, say). The days are stored with the database's subscription and every renewal moves on by exactly that many days; Amount / period is billed once per cycle. Subscriptions saved before Days existed keep renewing by calendar months until they are edited and saved. The company code is always stored in capitals.
Currency is picked from a searchable list (AUD, NZD, USD, EUR, GBP, SGD, INR, LKR and the Gulf currencies) and defaults to AUD; the same list is on Import invoice. Next renewal fills itself in from the start date and the billing cycle, and follows them when either changes — overwrite it only for an unusual first term.
| Field | Meaning |
|---|---|
| Grace days | Days the licence keeps working past its valid-to date. Carried inside the key. |
| Invoice net days | Payment terms. Each invoice is due this many days after its period starts. Once overdue the subscription shows past_due; nothing is blocked. |
| Dunning days | Extra days after the due date. If the invoice is still unpaid after due date + dunning days, the daily billing sweep (needs LICENSE_BILLING_ENABLED=true) suspends the subscription, and the customer’s next heartbeat blocks login and add-on modules until the invoice is marked paid, which lifts the suspension. |
To add modules, change the seat count, or adjust the price mid-term, open the customer and Edit the subscription. Saving re-issues the licence token with the new entitlement and delivers it to the customer automatically on their next heartbeat — even mid-term, when the expiry date is unchanged. For on-prem, send the new .lic from the customer's Issued keys list.
29.03 upgrade invoice). Collect it with a Payment link, the Charge saved button, or Mark paid. The console confirms the invoice on save.Run it any time with Settings → Run renewals now; it also runs daily when LICENSE_BILLING_ENABLED=true.
Ritzy's own accounts run on Ritzy One, so a subscription invoice can be raised there and imported here with its PDF:
- here and in the customer's Subscription & Billing). Company is typed in capitals and must be a valid Ritzy One company code; an invalid code is refused with a message rather than dropped, so the invoice never lands silently under the billing company. Tick Already paid in Ritzy One if it is settled.Importing records the invoice against the subscription only; it does not move the renewal date. To stop this server raising its own invoice at each renewal as well, edit the subscription and set Invoices to Imported from Ritzy One: renewals still extend the term and re-sign the licence, but raise nothing. A PDF can be attached or replaced later from the invoice row, and an open invoice raised in error can be Voided.
When one licence is installed into several companies of an install, every company presents the same customer. Invoices show only in the subscription's billing company (each invoice can name a different one on import); the other companies' Subscription & Billing screens say where the invoices are. The first company to open that screen becomes the billing company; change it with Edit → Billing company, which lists the company codes the install has reported. The code is typed in capitals; one that is not a valid Ritzy One company code is refused with a message.
Optional. Two flows, both run from the License Server: a hosted payment link per invoice, and off-session auto-charge of a saved PayPal method on renewal. Payers authenticate on PayPal's own pages — card and account details never reach Ritzy; only an order reference and, if the payer opts in, a saved-method token are stored.
developer.paypal.com → Apps & Credentials. Choose Sandbox (testing) or Live, then Create App, and copy the Client ID and Secret..env set PAYMENT_GATEWAY=paypal, PAYPAL_ENV=sandbox (or live), PAYPAL_CLIENT_ID, PAYPAL_SECRET, and PUBLIC_BASE_URL (this server's public URL, reachable by PayPal — https in production). Keep secrets in .env only.https://<server>/api/webhooks/paypal and subscribe to Payment capture completed (PAYMENT.CAPTURE.COMPLETED). Copy the Webhook ID into PAYPAL_WEBHOOK_ID. Every event is signature-verified against this id; unverified events are rejected./api/license/billing on this server, so your PayPal secret never leaves the License Server and each install can only see and pay its own invoices.PAYPAL_ENV=live with the live values and an https PUBLIC_BASE_URL, and restart.PAYMENT_GATEWAY=none the manual mark-paid model is used and none of the above is required.The Enquiries page is the sales inbox. Website quotation requests arrive here automatically once intake is set up (below), and you add phone and email leads by hand. A company created on an installation that holds no licence yet does not come here: it goes straight under Customers (see §1). Older enquiries with source install date from before that change. From an enquiry you go on to a proposal (§10) or link it to a customer.
| Part | What it does |
|---|---|
| Status tabs | New, Contacted, Quoted, Won, Lost, Closed and All, each with its count. The page opens on New. |
| Menu badge | The Enquiries menu item shows how many are new, so a website enquiry is noticed without opening the page. It clears as you move them on. |
| Search | Matches name, company, email, phone and message text. |
| Columns | Received, From, Type (quotation or question), Industry, Country, Seats, Modules, Source (website, manual, email-paste or install), Status, and the linked Customer. |
| Header tag | website intake on once the intake key is set; website intake off until then. |
Click Open (or double-click a row). The drawer shows every field in readable form (country and module names rather than codes, mail and phone links), and:
New enquiry opens the phone / email lead form: Contact (type, name, company, email, phone — an email or a phone number is required), Requirement (industry, country, modules, users, hosting, billing, start, data migration, online demo and demo time) and Notes (Their words and Our notes). It is saved with source manual.
For when the website's automatic post did not arrive, or the lead came in by email or message. AI Paste sits in the title of the New enquiry form and on the Customer card of the proposal editor. Paste the text and click Read it.
send.php sends to the enquiries mailbox, in either its quotation or question layout) is parsed exactly, without AI. No Anthropic key is needed for it.ANTHROPIC_API_KEY. Check every value.email-paste, and the form shows a reminder to check every field.The website's quotation form (send.php) still emails every enquiry; with intake set up it also posts the same fields to this server. The post runs after the email is sent, is capped at 5 seconds and ignores every failure, so a License Server that is down or misconfigured never blocks the visitor or stops the email.
openssl rand -hex 32.ENQUIRY_INTAKE_KEY to that key in .env, plus BEHIND_CLOUDFLARE=true when the server is proxied by Cloudflare (the live one is), and restart. install.cmd asks for all three intake settings (key, posts per minute, Cloudflare) and can generate the key. The endpoint POST /api/enquiries/intake needs the key in the X-Enquiry-Key header. While the key is unset it answers 404 and nothing is exposed. The Enquiries page then shows website intake on.ritzy-config.php one level above the site root (never in the web root, never in git), returning enquiry_intake_url (https://lic2.ritzy.net/api/enquiries/intake) and enquiry_intake_key (the same key). If the host does not allow files above the web root, use a private/ folder denied by .htaccess. With the file missing or empty, send.php simply skips the post.send.php — paste the snippet from packages/license-server/website-intake.md straight after the mail() call. That file has the full snippet, the example config and the Cloudflare steps.POST /api/enquiries/intake: there is no browser to solve a challenge. Keep SSL/TLS on Full (strict).curl POST with the key answers {"ok":true,"id":N}; a wrong key gives 401, and a server with no key 404.ENQUIRY_INTAKE_PER_MINUTE). The URL must be https, because the key travels in a header. Behind Cloudflare the client address is read from CF-Connecting-IP, but only for requests that really come from Cloudflare's edge ranges, so it cannot be forged by calling the origin directly. No CORS change is needed: the post is server-to-server.Ritzy enquiry intake failed:. A Cloudflare challenge shows there as HTTP 403 or 503 with HTML. The email has still gone, so paste it in with AI Paste meanwhile.A proposal is what the customer receives: a cover letter written for them, the features of the modules it sells, an implementation & training timeline, and the investment. (Proposals were called quotations until October 2026; old /quotations links open the same proposal, and the numbers stay RQ-YYYY-NNNN.)
Proposals are priced from a versioned rate card by one shared pricing module that both the console and the server use. The breakdown you see while editing is computed in the browser; on save the server prices the same inputs again against the same rate card and stores its result, so a total can never be typed in.
Proposals → New proposal, Create proposal on an enquiry, or New proposal on a customer's Proposals tab (each pre-fills what it knows). The tag at the top shows the rate card in use, e.g. Rate card v1 · from ….
| Card | Fields |
|---|---|
| Calculator | Concurrent seats (slider, or type a larger number); Modules — Accounts is in every seat, and each module is chosen on its own; Billing — Monthly or Annual · 12 for N; Tax — Australia · GST or Export · GST-free; Hosting — On-premises or Ritzy Cloud. |
| Customer | From enquiry pre-fills the calculator and contact from an enquiry; Customer picks an existing customer (optional until acceptance) and fills the details; then Prepared for (required), Attention, Address, Email. AI Paste is on this card (§9). |
| Cover letter | Write with AI drafts the letter with Claude from the customer's own message on the linked enquiry (shown above the box) and what the proposal sells: modules, seats, hosting and billing. No enquiry message? Paste customer's message takes an email or chat for the draft (it is not saved). The AI is told to use only those facts and not to quote prices; read and edit the draft — it is saved with the revision exactly as you leave it. Optional: leave it empty for no letter. |
| Implementation & training timeline | Suggested phases — kick-off & planning, installation (or cloud set-up) & data, configuration, user training, parallel run & go-live, post go-live support — scaled by the add-on modules, the seats, on-premises vs cloud, and whether the enquiry asked for data migration. It follows your choices until you edit it; change titles, weeks and text, reorder, add or remove phases (up to 12), or Reset to the suggested plan. |
| Discount / price override | Special discount % off the software subscription (platform and modules, after the bundle discount; not hosting or one-time charges); Line overrides replace a line's price (subscription lines per month, the annual multiplier still applies); Reason is required with either and is printed on the proposal. |
| Validity & terms | Valid for N days (default 30; the valid-until date is shown); Terms, one per line — they follow your choices until you edit them, {validUntil} prints as the date, and Reset to the defaults brings back the standard terms. |
| Breakdown | The live price breakdown, updated with every change, with Preview (unsaved draft) and Save proposal. |
Saving gives the proposal its number, RQ-YYYY-NNNN (drawn in sequence per year), as Rev 1.
draft, Edit changes them in place (wording only; prices stay as priced). Once sent or decided they are fixed: Revise to change them.| Status | Meaning |
|---|---|
draft / sent | Set by you; a new revision goes back to draft. |
expired | Worked out automatically: a draft or sent proposal past its valid-until date. It cannot be accepted until you Revise it to re-price and re-date it. |
superseded | Every revision older than the latest, unless it was the accepted one. |
accepted / rejected | Set by you. The list's subscribed tag shows an accepted proposal that has become a subscription. |
Once marked accepted, the page offers Create subscription. The dialog is pre-filled from the accepted revision: customer (existing, or a new one with a Customer code typed in capitals), plan, seats, modules, cycle, amount per period excl. GST, currency and start date. Create subscription + sign licence goes through the normal billing path (§7): it signs the licence and raises the first invoice. One-time charges are not part of the subscription; invoice them separately. If the customer already has a subscription, the dialog warns that saving updates it to these terms (with a prorated invoice when the amount goes up mid-term). The button is disabled until a signing key exists.
Print preview (or Print on a revision row) opens the stored revision in its own window with a toolbar: Print / Save as PDF and Close. A superseded revision is marked so in the toolbar. For a PDF, pick Save as PDF as the printer; the file is named after the proposal number, revision and customer. On screen a dashed line marks each page break. The document, on A4:
The features and the investment each start a new page; tables, totals, the timeline chart and the signature block are never split across pages, and every page after the cover carries Page n of N.
Revisions written before proposals print without a letter or timeline. The module feature lists come from src/shared/proposal.ts in the License Server.
Proposals filters by status (All, Draft, Sent, Accepted, Rejected, Expired) and searches by number or customer. The same list, limited to one customer, is the Proposals tab on that customer's page, beside Licence & billing.
The rate card the calculator prices with, before GST: platform seat (incl. Accounts), each module per seat, seat bands (each seat pays its band's share of the full rate), bundle discounts for 3–4 and 5+ modules, annual billing as 12 months for N, GST %, one-time implementation (set-up plus per module) and training per seat, and Ritzy Cloud hosting tiers with a cloud set-up fee. Version 1 is seeded with the draft AUD rates.
| On-premises | SaaS / Ritzy-hosted | |
|---|---|---|
| Key delivery | Send the .lic file / paste the key; customer installs it once | Same first install, then renewals arrive automatically via heartbeat |
| Revoke / suspend | Takes effect at token expiry only (no heartbeat) | Immediate (next heartbeat) |
| Auto-renew delivery | Re-issue and send a new token | Delivered automatically via heartbeat |
| Enforcement strength | Tamper-evident (signed), but the app runs on the customer's server | Strong — you control the runtime |
The same signed-token core serves both; only the delivery and the heartbeat differ by configuration. An offline install keeps working on its signed token plus the grace window until the next successful heartbeat.
The customer installs the key you sent from Tools → DBA Console → Licensing. Thereafter, when a licence needs attention an app-wide subscription banner appears automatically (silent when the licence is healthy):
| Situation | Customer experience |
|---|---|
| Expiring within 14 days | Dismissible reminder to renew |
| In grace (just expired) | Warning with the date access continues until |
| Expired | Login blocked, with a renew message. An administrator can still sign in, to Subscription & Billing only, and pay |
| Suspended | Login blocked and modules cut off, showing your reason. An administrator can still sign in, to Subscription & Billing only, and pay — paying lifts the suspension |
| Revoked | Login blocked for everyone, administrators included; billing is refused too |
| Module not licensed | The module is hidden from the menu; its screens are unavailable |
| All seats in use | “Maximum concurrent users reached” on login |
Set in the License Server's .env. This is the only box that holds the signing key.
| Setting | Purpose |
|---|---|
PORT | HTTP port for the API + console (default 4100). |
LICENSE_FB_DATABASE | The licence database, a Firebird file (default ./data/LICENSE.FDB). Created on the first start. |
LICENSE_FB_HOST / _PORT / _USER / _PASSWORD | The Firebird server. Empty = the FB_* values in the environment, else localhost, 3050, SYSDBA, masterkey. |
LICENSE_DB_FILE | The SQLite file of a server from before Firebird (default ./data/license.db). Copied into an empty Firebird database on its first start, then left untouched. |
LICENSE_BACKUP_DIR | Where gbak backups (.fbk) go (default ./data/backups). |
LICENSE_BACKUP_KEEP | Daily backups to keep (default 14). 0 turns the daily backup off. |
GBAK_PATH | Only if gbak is not found from the Firebird install. |
ADMIN_JWT_SECRET | Secret for admin login sessions. Set a long random value so sessions survive restarts. |
LICENSE_SIGNING_KEY / _FILE | Private key to mint tokens. Normally created & stored by Settings → Generate key pair. Never place on a customer install. |
LICENSE_PUBLIC_KEY_FILE | Optional explicit public key; otherwise derived from the private key. |
BOOTSTRAP_ADMIN_USER / _PASSWORD | First-run admin seed. Change the password after first login. |
LICENSE_BILLING_ENABLED | true runs the daily auto-renew / dunning sweep. |
PAYMENT_GATEWAY | paypal enables online payments; none (default) keeps the manual mark-paid model. |
PUBLIC_BASE_URL | Public URL of this server; builds PayPal return / webhook URLs (https in production). |
PAYPAL_ENV | sandbox or live. |
PAYPAL_CLIENT_ID / PAYPAL_SECRET | PayPal REST app credentials (secret — never committed). |
PAYPAL_WEBHOOK_ID | Webhook id used to signature-verify incoming payment events. |
ENQUIRY_INTAKE_KEY | Shared key the website sends in X-Enquiry-Key. Empty (default) = intake off and /api/enquiries/intake answers 404. See §9. |
ENQUIRY_INTAKE_PER_MINUTE | Intake posts accepted per minute from one address (default 30). |
BEHIND_CLOUDFLARE | true when this server is proxied by Cloudflare: the intake then reads the client address from CF-Connecting-IP, only for requests from Cloudflare's edge ranges. |
ANTHROPIC_API_KEY | Needed for AI Customer Support (§14) and for AI Paste of free text. The website's own enquiry email is parsed without it. |
Set in each customer's Ritzy One API .env. These carry only the public key — no signing capability.
| Setting | Purpose |
|---|---|
LICENSING_ENABLED | Normally left unset so the DBA Console Enforcement switch controls it. Setting it to true or false pins enforcement and disables the switch. Enforcement also needs a valid token. |
LICENSE_PUBLIC_KEY / _FILE | Public key to verify installed tokens. Optional — normally set from the console via Licensing → Trusted public key instead. If set here, this env value takes precedence and pins the key (the console shows it as environment-pinned and read-only). |
LICENSE_PUBLIC_KEY_STORE | Where the console-managed trusted key is saved. Default <app>/data/license-public.pem; override only if you keep app data elsewhere. |
LICENSE_HEARTBEAT_URL | The License Server's /api/license/heartbeat URL. Optional — normally set from DBA Console → Licensing → License Server URL instead. If set here, this env value takes precedence and pins it. Unset and never saved in the console = the live server https://lic2.ritzy.net; cleared in the console = no heartbeat (pure on-prem). |
LICENSE_HEARTBEAT_INTERVAL_MIN | Heartbeat interval, minutes (default 60). |
data\LICENSE.FDB, backed up daily with gbak and restorable from Settings → Database backup. On each customer install, the installed token and its last heartbeat result are stored in that company's WEB_LICENSE table.The License Server is also the brain behind the Support button inside every customer's Ritzy. Customer installs do not answer support questions themselves — they forward them here over the licence token they already hold, and this box does the AI call, the search and the learning. That is deliberate: no customer needs an Anthropic key, you keep one bill and one prompt to improve, and every question from every site lands in one corpus.
Setting (License Server .env) | Purpose |
|---|---|
ANTHROPIC_API_KEY | The vendor's key. Support is off until this is set. This is the only box in the whole system that needs one. |
SUPPORT_ENABLED | Master switch (default true). Set false to take support offline without removing the key. |
DOCS_BASE_URL | Where the published manuals live — the assistant deep-links their sections. Default https://www.ritzy.net.au/docs/web. Must match the live site. |
SUPPORT_RATE_PER_HOUR / _PER_DAY | Per-customer question budget (default 60 / 300), so a looping install cannot run up the bill. Counted from stored messages, so it survives a restart. |
A customer install needs nothing configured at all: if it has a valid licence and can reach this server, the Support button appears. If it cannot, the button hides itself.
| Page | What it is for |
|---|---|
| Support | Every conversation from every customer. Filter to Needs a human first — those are the questions the assistant could not answer confidently, or that a customer marked unhelpful. Open one to read the whole exchange with the company, user, module and build stamped on it. |
| Knowledge Base | Your answers. These outrank the user manuals in search, so writing one is how you correct a wrong or missing answer for every customer at once, without waiting for a manual to be rewritten and re-uploaded. |
| Feature Requests | Gaps the assistant hit while answering, grouped so the count is distinct customers asking, not distinct asks. Each row keeps the conversations and the customers' own words. |
Three corpora are searched together and ranked against each other — knowledge-base articles first, then manual sections, then past answers. Two things feed the last of those:
| Action | Effect |
|---|---|
| A customer clicks 👍 | That answer joins the searchable corpus, so the next person asking something similar gets a reply grounded in one that already worked. |
| A customer clicks 👎 | It is pulled straight back out and the conversation is flagged Needs a human. A bad answer can never teach the next one. |
On a good conversation, Draft article in the thread drawer asks the assistant to turn it into a knowledge-base article. Read it before publishing — a published article is repeated to every customer who asks something similar, so a mistake there is expensive.
npm run docs:catalogue in packages/license-server, then rebuild), and reloads on every boot — the button is for doing it without a restart.Any customer can press Talk to a person in the Support panel (web app, every phone app, and the Ritzy Support app). The assistant then stops answering that conversation and it waits for you. You can also join a conversation nobody asked you into — for example one that is going badly.
| You see | What it means / what to do |
|---|---|
A red count on Support in the menu, and (n) in the browser tab title | Customers waiting for a person. Open Support → Show live chats (or the Live chat filter). Waiting rows sort first, longest wait first. |
| waiting for a person / with sam | The live state, shown instead of the triage status. A green dot means the customer has the conversation open right now. |
| Take over | You join; the customer sees “name from Ritzy Support joined”. Simply typing a reply does the same. If a colleague already has it you are asked before taking it from them — two agents in one chat reads as chaos from the customer's side. |
| Hand back to AI | You leave; the assistant answers the next message, and it reads what you said as Ritzy staff, so it carries on from your answer rather than contradicting it. The thread is marked answered. |
| customer online / away (drawer title) | Away means nobody is reading. Your reply is still stored and shows in the customer's panel the next time they open it and in the Ritzy Support app's History. |
| Rephrase before sending | On by default. Send rewrites your reply with AI and leaves it in the box without sending it; read it, change anything, and press Send again. Undo brings back what you typed. Rephrase does the same on demand with the switch off. If the AI cannot be reached you are told, and Send sends what you typed. |
| Attach | Send the customer a file; theirs appear in the conversation the same way. Images, PDF, text, CSV, Word, Excel and zip files, up to 5 MB each and 30 per conversation. Sending a file takes the conversation over, as replying does. |
| WhatsApp call | Enter the customer's number once and WhatsApp opens on it; press its call button to ring them. WhatsApp has no link that places the call itself. The number is kept with the conversation, offered again for that user's later conversations, and never shown in the customer's app. |
| Raise ticket | Files the conversation in Escalated Tickets as CHAT-number, for work that will outlast the chat. The form opens with an AI summary and the transcript (just the transcript if the AI is unavailable). Images from the chat become the ticket's screenshots, up to six. The customer sees a line with the ticket number, and your reply on the ticket comes back to this conversation. |
Phone notifications. In the Ritzy Licensing app, turn them on from the Support tab or More. With the app closed you are told when a customer asks for a person, when the customer in a conversation you are handling writes, and when a ticket is escalated. Who is told follows the role: Support inbox & live chat for chats, Escalated tickets for tickets. Tapping one opens that conversation or ticket. There is nothing to configure: the server creates its own push keys in its data folder (push-vapid.json). If that file is lost, everyone turns notifications on again. On an iPhone the app must be installed to the home screen first. A question the assistant answers does not notify anyone.
Set SUPPORT_WHATSAPP in the server's .env to give customers a Call Ritzy on WhatsApp link in their chat. It stays off in Ritzy Dues, whose reader may be your customer's own customer.
Being signed in to this console is being available: while any page of it is open, customers who ask for a person are told someone will join shortly; when nobody has it open they are told honestly that their conversation is queued. Closing a conversation (status Closed) also ends the live chat.
ANTHROPIC_API_KEY of its own, and conversations stay scoped to one user of one customer.Every person who uses the console gets their own login with a role, so the tool can be handed to Accounts, Sales and Support without giving everyone the signing key. Manage them in User Manager (Administrators only).
| Role | Sees / can do |
|---|---|
| Administrator | Everything, including users and the signing key. Cannot be edited. |
| Accounts | Customers (read), subscriptions, invoices, payment links, saved methods, the billing run. |
| Sales | Customers (read), Enquiries, Proposals and the rate card. |
| Support Agent | Support inbox, live chat and Escalated Tickets. No customer or billing data. |
| Support Lead | As Support Agent, plus Knowledge Base, Feature Requests, Rebuild index and customers (read). |
| Licensing | Customers, licence keys, activation codes, trials, suspend/revoke. |
| Read only | Dashboard and customers, nothing else. |
A role is a set of permissions: View customers, Manage licences, Manage billing, Sales, Support inbox & live chat, Escalated tickets, Knowledge base, Server settings, Database backup & restore, Users & roles. Only Administrators have Database backup & restore until you give it to another role. Built-in roles other than Administrator can be edited, and New role makes your own (e.g. “Collections” = View customers + Manage billing). A change applies to everyone with that role on their next click.
| Situation | Do |
|---|---|
| Leaves Ritzy | Switch Active off. They are signed out on their next click — not when their 8-hour session expires. Prefer this to Delete: their name stays meaningful on past live-chat replies and in the audit log. |
| Forgot their password | Key icon → a new temporary password, shown once. |
| Lost their authenticator | Shield icon → removes 2FA; they sign in with the password and enrol again. |
reset-admin.cmd <username> <newPassword> --make-admin: it resets the password, re-enables the account and makes it Administrator.User Manager → Audit log records sign-ins, failed sign-ins, and every user and role change, with who did it and from which address. It cannot be edited from the console.
BOOTSTRAP_ADMIN_USER) must change its password at first sign-in. New passwords need at least 8 characters.Everything the License Server knows — customers, subscriptions, invoices and their PDFs, licence keys, activation codes, support conversations, console users and the audit log — is in one Firebird database, data\LICENSE.FDB, on the Firebird server installed on the same machine (Firebird 3, 4 or 5). This is the vendor’s own server only: customer installs of Ritzy One run on PostgreSQL and need no Firebird. The License Server creates it, with all its tables, the first time it starts. Its connection is set by LICENSE_FB_* in .env (§13).
Servers installed before 30 October 2026 kept their data in a SQLite file, data\license.db. To upgrade:
.env; if the SYSDBA password is not masterkey, set LICENSE_FB_PASSWORD.license.db is copied into it, with the same customer codes, Company IDs and record numbers. The log shows each table and how many rows came across.license.db itself is only read, never changed. Keep it until you are satisfied, then delete it.
Backups are made with Firebird's own tool, gbak, while the server keeps running. They are .fbk files in data\backups (LICENSE_BACKUP_DIR), and appear in the License Server under Settings → Database backup:
| Kind | When |
|---|---|
| Daily | Every day, automatically. The newest 14 are kept (LICENSE_BACKUP_KEEP; 0 turns it off). |
| Manual | Back up now. Never deleted automatically. |
| Before restore | Taken automatically just before every restore — this is how a restore is undone. |
| Uploaded | An .fbk you uploaded with Upload, e.g. to move the server to a new machine. |
Each backup can be downloaded. Copy backups off the machine too: a backup on the same disk does not survive the disk.
To wipe a test License Server and install again, run uninstall.cmd -Yes -PurgeData as Administrator from its folder: it removes the service and deletes .env and the whole data\ folder (database, backups and the signing key pair), then run install.cmd. Never on the live server — without the private key no licence ever issued can be verified. Full steps, for the customer install too, are in the installation runbook, §13.
RESTORE.To undo a restore, restore its Before restore backup. A restore replaces everything, console users included, so a user created after the backup was taken no longer exists afterwards. Backups, uploads, deletions and restores are written to the audit log (§15).
.env and the signing key files from data\, start it once, then Upload the latest backup from the old machine and Restore it.| Symptom | Likely cause / fix |
|---|---|
| “Your role does not allow this” / a page is missing from the menu | That user's role lacks the permission. An administrator changes the role in User Manager; it applies at once. |
| License Server stops at start with “could not open its database” | Firebird is not running, or the LICENSE_FB_* settings are wrong (usually the password). Start the Firebird service, check .env, and restart. See §16. |
| Back up now and Restore are greyed out | gbak was not found. It is installed with the Firebird server; if it is somewhere unusual, set GBAK_PATH in .env and restart. |
| Can't issue keys / subscriptions | No signing key yet. In the License Server, Settings → Generate key pair. |
| “Could not be verified” when the customer installs a key | The install trusts no public key, or the wrong one. On the install, go to Licensing → Trusted public key → Fetch & trust (or paste the PEM) to trust the License Server's current key, then retry. Confirm the fingerprints match (Licensing tab vs. Settings). If you rotated the signing key after issuing the token, re-issue the licence — a token signed by a replaced key can't be revived. |
| Enforcement not happening | LICENSING_ENABLED is off on the install, or no valid token is installed for that company and its 10-day trial has not ended yet. |
| “This copy of Ritzy is not licensed and its trial period ended” | Enforcement is on and no valid licence is installed. Sign in as an administrator (or DBA) — still allowed — and enter the activation code on the Activate Ritzy screen, or under DBA Console → Licensing. |
| A module the customer paid for is hidden | Edit the subscription to include it — the re-issued token arrives on the next heartbeat (mid-term too, not only at renewal). For on-prem, send the new .lic. If it still isn't showing, the install hasn't heartbeated yet or LICENSE_HEARTBEAT_URL isn't set. |
| Revocation not taking effect | The install hasn't heartbeated yet (up to the interval), or LICENSE_HEARTBEAT_URL isn't set on it. |
| Auto-renew not running | LICENSE_BILLING_ENABLED off, or no signing key. Use Run renewals now to test. |
| Payments card says “not configured” | PAYMENT_GATEWAY isn't paypal, or PAYPAL_CLIENT_ID / PAYPAL_SECRET are missing. Set them and restart the License Server. |
| Customer's Subscription & Billing says “not connected to the Ritzy licence server” | The install has no heartbeat URL (a key pasted by hand leaves it blank). Set it in DBA Console → Licensing, or activate with a code. |
| Invoice paid on PayPal but still open | Webhook not reaching the server or PAYPAL_WEBHOOK_ID wrong (events fail verification). Check the webhook URL /api/webhooks/paypal and that PUBLIC_BASE_URL is publicly reachable. |
| Website enquiries not reaching Enquiries | Header shows website intake off: set ENQUIRY_INTAKE_KEY and restart. Otherwise check the website's PHP error log for Ritzy enquiry intake failed: — 401 is a key mismatch with ritzy-config.php, 404 an unset key, 403/503 with HTML a Cloudflare challenge (add the WAF skip rule). Paste the email in with AI Paste meanwhile. |
| AI Paste fails on a forwarded mail or message | Free text needs ANTHROPIC_API_KEY on this server. The website's own enquiry email works without it. |
| Write with AI greyed out on a proposal | The cover letter needs ANTHROPIC_API_KEY on this server (optionally PROPOSAL_AI_MODEL to choose the model). Write the letter by hand meanwhile; it prints the same. |
| Proposal cannot be marked accepted | It has expired. Revise it to re-price and re-date it, then accept the new revision. |
| “… is not a company code” on import or subscription edit | The Company / Billing company must be a Ritzy One company code: up to 20 letters, digits, - and _. Pick one the install has reported, or leave it empty for the billing company. |
| “Charge saved” unavailable | The customer has no saved method yet — send a Payment link first so they can save it during checkout. |
| No Support button in a customer's Ritzy | That install has no valid licence token, or cannot reach this server. Check its DBA Console → Licensing and that SUPPORT_ENABLED + ANTHROPIC_API_KEY are set here. GET /api/license/support/health answers {"enabled":true} when this side is ready. |
| Customers get “Support AI is not configured” | ANTHROPIC_API_KEY missing or invalid on this server. The boot log says so explicitly. |
| Answers tagged “offline answer” | The install could not reach this server and fell back to its own key. Check network and LICENSE_HEARTBEAT_URL on that install — the support URL is derived from it. Offline answers are never stored here. |
| Assistant cites the wrong manual section | Write the right answer in Knowledge Base — articles outrank manuals. If a manual was restructured, re-run npm run docs:catalogue, rebuild and redeploy, then Rebuild index. |
| A customer hit the question limit | Raise SUPPORT_RATE_PER_HOUR / _PER_DAY and restart, or check the Support inbox for a looping install. |