Ritzy One • Administrator / Vendor Guide — Internal

Subscription Licensing

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.

Applies to the Ritzy License Server + licensed installs  |  Internal / vendor tooling

1 How it works

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.

Safe by default
With enforcement off, nothing is enforced: the app behaves exactly as an unlicensed build (unlimited seats, all modules). This lets you deploy the code first and switch enforcement on later.
10-day trial when enforcement is on
With enforcement on and no valid token installed, the install runs as a 10-day trial (all modules, the legacy seat count). The trial starts the first time the server sees the install unlicensed; the start is recorded both in the install’s licence settings file and in the company database, and the earlier of the two counts, so deleting one does not restart it. When the trial ends, sign-in and session renewal are refused for ordinary users. The 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

2 The Ritzy License Server

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.

The console

ScreenWhat you do there
DashboardAt-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).
EnquiriesThe 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.
ProposalsThe 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.
CustomersThe 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.
RevocationsThe remote kill-switch list — suspend or revoke a customer by id, or re-activate.
SettingsGenerate / 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.

The phone app (Ritzy Licensing)

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.

  1. On the console Dashboard, find the Ritzy Licensing phone app card. It shows the install QR code, with Copy link, Open and Print QR.
  2. Scan the QR code with the phone and sign in with your License Server account.
  3. Add it to the home screen when the phone offers to install it.
HTTPS only
Phones install apps only over HTTPS. The QR code points at PUBLIC_BASE_URL when that is an https:// address, so set it (§13). Until it is, the card shows a warning.
TabWhat you do there
HomeWhat 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.
CustomersSearch, 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.
EnquiriesStatus tabs; call, WhatsApp or email the lead (which marks it contacted); status and notes; a new enquiry with AI Paste. See §9.
SupportThe live-chat queue (join, reply, hand back, close), the needs us inbox, and escalated tickets with photos, replies and internal notes. See §14.
MoreInvoices (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.

The customer side

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.

Access
Sign in to the License Server with its admin account (seeded on first run from BOOTSTRAP_ADMIN_USER / BOOTSTRAP_ADMIN_PASSWORD). Change that password immediately and enable two-factor — both in Settings.

3 One-time setup

  1. Deploy & run the License Server. Install dependencies, copy .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.
  2. Sign in & secure the account. Log in with the bootstrap admin, open Settings, change the password, and enable two-factor.
  3. Generate the signing key pair. In Settings → Licence signing key, click Generate new key pair. The private key is stored on this server (and shown once to back up); copy the public key.
  4. Trust the public key on every customer install — do it in the install itself: Tools → DBA Console → Licensing → Trusted public key. An install points at the live server 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.
  5. Point each install at the server — nothing to type for the live server: an install whose URL has never been set uses 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.)
  6. Turn enforcement on where you want it — the Enforcement switch on DBA Console → Licensing. It takes effect immediately, with no .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).
Key rotation
Generating a new pair replaces the old one on the server. Any licence still verified only by the old public key will stop verifying — so only rotate for a fresh setup or a deliberate migration, then re-issue affected licences and re-trust the new public key on each install (one click via Fetch & trust, or update the .env value if you pinned it there).

4 Issue & deliver a licence

In the License Server console:

  1. Create the customer — Customers → New customer. The Customer code you set here is the stable id embedded in every token and used by billing and revocation. It is typed in capitals, here and wherever a customer is created from an enquiry or a proposal. Deployment (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.
  2. Issue a key, either way:
    • As a subscription (recurring) — open the customer and create a Subscription (seats, modules, dates). Saving mints the token automatically and can auto-renew it later.
    • As a stand-alone key (one-off / on-prem) — open the customer and click Issue stand-alone key on the Company databases card (or Stand-alone key beside a database that registered itself, which fills in its company code); set seats, modules, valid-to and grace, and click Issue key. No subscription, auto-renew or invoice is created.
  3. Deliver it — send an activation code (the normal way). On the customer's page, Activation code → New activation code: choose how many times it may be used (1 for a single install), an optional expiry, and an internal note. The code — 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.
  4. The customer activates, either in the last step of the New Company wizard or, if that was skipped, signed in as 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.
    Administrators can activate too, without the DBA Console: any member of the 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.
Activation codes are bearer credentials
Whoever holds the code can pull that customer's licence, so codes are single-use by default, can be given an expiry, and can be revoked at any time from the same card; every redemption is logged with its IP. Send one like a password, and revoke an unlimited-use code once the install is finished.

Manual delivery (offline / air-gapped installs)

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.

FieldMeaning
Customer code / idStable identifier used by billing and revocation (e.g. ACME). Applies across all of that customer's installs.
Concurrent seatsMaximum simultaneous devices (browsers). Replaces the legacy hardware-key cap when a token is present.
ModulesThe paid add-ons: Cargo, Liner, Marine, Warehousing, Transport, Shipchandling, Payroll, CRM, Real Estate. Anything omitted is hidden and blocked.
Valid to / GraceThe term end, plus a grace period after which login is blocked.
Every key is logged
Each token minted — manual, re-issue, or auto-renew — is recorded in the customer's Issued keys audit list, and any past key can be re-downloaded as a .lic from there.

5 Seats & modules

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.

Gated vs core
The gated add-ons are Cargo (SP), Liner (LN), Marine (MA), Warehousing (WH), Transport (TR), Shipchandling (SC), Payroll (E), CRM (CR), Real Estate (RE). Accounts (AC) is the only core module; it is always available and not gated. Liner, Marine, Transport, Warehousing and Shipchandling each stand on their own; none needs Cargo.
Cargo before it became an add-on
Cargo used to be included without being listed in the licence. So that nobody loses it:
  • Every subscription that existed before the change was given Cargo automatically, so renewals, re-issues and plan changes keep it.
  • Licence keys issued before the change keep Cargo open at the customer.
  • Keys issued from now on carry a flag that makes the customer install enforce Cargo like any other add-on. Leave Cargo out of a new key only when the customer really has not bought it.
Companies created before July 2026
New companies install every module package, so entitlement is purely a licence matter as described above. Companies created by an older build got only Accounts, Cargo and Payroll in their 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.

6 Heartbeat & revocation

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.

Install sends its token→ Server checks the revocation list→ Returns active / suspended / revoked (+ a fresher token if renewed)→ Install applies & enforces

Suspend or revoke a customer

  1. Open the customer (or the Revocations page) and choose Suspend (temporary, e.g. non-payment) or Revoke (permanent), with a reason.
  2. On its next heartbeat the customer is blocked, with your reason shown, and add-on modules are cut off. Click Re-activate to restore.

Enable it

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).

Fail-open by design
If the server is unreachable, or returns something the install can't verify, the install keeps its previous state — a network blip never locks out a paying customer. Only an explicit suspended/revoked blocks access.

7 Billing & auto-renew

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.

Create a subscription

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.

FieldMeaning
Grace daysDays the licence keeps working past its valid-to date. Carried inside the key.
Invoice net daysPayment terms. Each invoice is due this many days after its period starts. Once overdue the subscription shows past_due; nothing is blocked.
Dunning daysExtra 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.

Change a plan (upgrade / downgrade)

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.

When proration applies
Only when the billing cycle and renewal date are unchanged and the price increased. Changing the cycle (e.g. monthly → annual) starts a fresh term billed in full at the next renewal, with no proration; adding a module without raising the amount also raises nothing.

What the daily sweep does

Run it any time with Settings → Run renewals now; it also runs daily when LICENSE_BILLING_ENABLED=true.

Manual by default, PayPal optional
With no gateway configured, renewals raise an open invoice you mark paid — no card is charged. Configure the PayPal gateway (next section) to accept online payments and auto-charge saved methods. Either way, card and account data never touch Ritzy servers.

Invoices raised in Ritzy One (import)

Ritzy's own accounts run on Ritzy One, so a subscription invoice can be raised there and imported here with its PDF:

  1. Raise the invoice in Ritzy One and save its PDF.
  2. On the customer's page, Invoices → Import invoice: enter the Ritzy One invoice number, invoice and due dates, amount and a description, and drop in the PDF. The Subscription period is optional and starts empty: fill both ends if the invoice covers a term, or leave it blank (the period then shows as - 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.
  3. The customer's administrator sees it under Tools → Subscription & Billing, opens the PDF, and can pay it with PayPal. Dunning treats it like any other invoice.

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.

Several companies on one licence

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.

8 Online payments (PayPal)

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.

  1. Create a PayPal REST app — sign in at developer.paypal.com → Apps & Credentials. Choose Sandbox (testing) or Live, then Create App, and copy the Client ID and Secret.
  2. Configure the License Server — in its .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.
  3. Add the webhook — in the PayPal app → Webhooks → Add Webhook, set the URL to 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.
  4. Restart & confirm — restart the server, then in the console → Settings → Payments it should read “PayPal is configured (sandbox/live)”.
  5. Take a payment — open a customer → an open invoice → Payment link. Send the link to the customer; on payment the invoice flips to Paid automatically (captured on return and confirmed by the webhook), lifting any dunning suspension.
  6. Customers pay from inside Ritzy — nothing to send. A customer administrator opens Tools → Subscription & Billing in their own Ritzy, sees the plan and invoices, and presses Pay with PayPal on an open one. PayPal opens in a new window; once it confirms, the screen updates by itself and pulls the renewed licence at once. The install uses its licence token to call /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.
  7. Enable auto-charge — the payment link asks the payer to save their PayPal method. Once saved (Saved payment method: on file), the daily sweep and the Charge saved button auto-charge future renewals with no customer action. Use Forget to remove it.
  8. Go live — after sandbox testing, create Live credentials and a live webhook, set PAYPAL_ENV=live with the live values and an https PUBLIC_BASE_URL, and restart.
Idempotent & fail-safe
Webhook deliveries are deduplicated by PayPal event id, so a repeated delivery never double-applies. With PAYMENT_GATEWAY=none the manual mark-paid model is used and none of the above is required.

9 Enquiries

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.

Enquiry (website / manual / email paste / install)→ Proposal (revisions)→ Accepted→ Subscription + signed licence + first invoice

The inbox

PartWhat it does
Status tabsNew, Contacted, Quoted, Won, Lost, Closed and All, each with its count. The page opens on New.
Menu badgeThe 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.
SearchMatches name, company, email, phone and message text.
ColumnsReceived, From, Type (quotation or question), Industry, Country, Seats, Modules, Source (website, manual, email-paste or install), Status, and the linked Customer.
Header tagwebsite intake on once the intake key is set; website intake off until then.

The detail drawer

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:

Adding a lead by hand

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.

AI Paste

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.

Website intake setup

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.

  1. Generate a shared key — e.g. openssl rand -hex 32.
  2. License Server — set 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.
  3. Website config — create 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.
  4. 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.
  5. Cloudflare — add a WAF custom rule that skips managed rules, rate limiting and bot checks for POST /api/enquiries/intake: there is no browser to solve a challenge. Keep SSL/TLS on Full (strict).
  6. Test — a curl POST with the key answers {"ok":true,"id":N}; a wrong key gives 401, and a server with no key 404.
What the intake accepts
Only the fields and values the website form itself allows (anything else is refused with 400), a body of up to 32 KB, and 30 posts a minute from one address (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.
When website enquiries stop arriving
The website logs every failed post to its PHP error log, prefixed 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.

10 Proposals & pricing

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.

Write a proposal

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 ….

CardFields
CalculatorConcurrent 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.
CustomerFrom 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 letterWrite 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 timelineSuggested 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 overrideSpecial 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 & termsValid 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.
BreakdownThe 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.

The proposal page

StatusMeaning
draft / sentSet by you; a new revision goes back to draft.
expiredWorked 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.
supersededEvery revision older than the latest, unless it was the accepted one.
accepted / rejectedSet by you. The list's subscribed tag shows an accepted proposal that has become a subscription.

Accepted → 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

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:

  1. Cover page (page 1, edge to edge) — logo, “Ritzy One for customer”, seats, modules, hosting, billing and the length of the implementation plan; prepared for, prepared by, number, revision, date and valid-until.
  2. Cover letter, when there is one, from page 2, addressed to the customer.
  3. Your Ritzy One solution — a card per module with its features: Accounts marked Included (the core of every seat), each selected add-on marked Add-on.
  4. Implementation & training timeline — a week-by-week chart of the phases and what happens in each.
  5. Investment — the subscription and one-time lines, any discount note, and the totals with GST and the first invoice.
  6. Terms and an acceptance block for both parties.

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.

The proposal list

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.

Settings → Pricing (the rate card)

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.

11 On-prem vs SaaS

On-premisesSaaS / Ritzy-hosted
Key deliverySend the .lic file / paste the key; customer installs it onceSame first install, then renewals arrive automatically via heartbeat
Revoke / suspendTakes effect at token expiry only (no heartbeat)Immediate (next heartbeat)
Auto-renew deliveryRe-issue and send a new tokenDelivered automatically via heartbeat
Enforcement strengthTamper-evident (signed), but the app runs on the customer's serverStrong — 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.

12 What the customer sees

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):

SituationCustomer experience
Expiring within 14 daysDismissible reminder to renew
In grace (just expired)Warning with the date access continues until
ExpiredLogin blocked, with a renew message. An administrator can still sign in, to Subscription & Billing only, and pay
SuspendedLogin blocked and modules cut off, showing your reason. An administrator can still sign in, to Subscription & Billing only, and pay — paying lifts the suspension
RevokedLogin blocked for everyone, administrators included; billing is refused too
Module not licensedThe module is hidden from the menu; its screens are unavailable
All seats in use“Maximum concurrent users reached” on login

13 Settings reference

License Server (the vendor box)

Set in the License Server's .env. This is the only box that holds the signing key.

SettingPurpose
PORTHTTP port for the API + console (default 4100).
LICENSE_FB_DATABASEThe licence database, a Firebird file (default ./data/LICENSE.FDB). Created on the first start.
LICENSE_FB_HOST / _PORT / _USER / _PASSWORDThe Firebird server. Empty = the FB_* values in the environment, else localhost, 3050, SYSDBA, masterkey.
LICENSE_DB_FILEThe 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_DIRWhere gbak backups (.fbk) go (default ./data/backups).
LICENSE_BACKUP_KEEPDaily backups to keep (default 14). 0 turns the daily backup off.
GBAK_PATHOnly if gbak is not found from the Firebird install.
ADMIN_JWT_SECRETSecret for admin login sessions. Set a long random value so sessions survive restarts.
LICENSE_SIGNING_KEY / _FILEPrivate key to mint tokens. Normally created & stored by Settings → Generate key pair. Never place on a customer install.
LICENSE_PUBLIC_KEY_FILEOptional explicit public key; otherwise derived from the private key.
BOOTSTRAP_ADMIN_USER / _PASSWORDFirst-run admin seed. Change the password after first login.
LICENSE_BILLING_ENABLEDtrue runs the daily auto-renew / dunning sweep.
PAYMENT_GATEWAYpaypal enables online payments; none (default) keeps the manual mark-paid model.
PUBLIC_BASE_URLPublic URL of this server; builds PayPal return / webhook URLs (https in production).
PAYPAL_ENVsandbox or live.
PAYPAL_CLIENT_ID / PAYPAL_SECRETPayPal REST app credentials (secret — never committed).
PAYPAL_WEBHOOK_IDWebhook id used to signature-verify incoming payment events.
ENQUIRY_INTAKE_KEYShared key the website sends in X-Enquiry-Key. Empty (default) = intake off and /api/enquiries/intake answers 404. See §9.
ENQUIRY_INTAKE_PER_MINUTEIntake posts accepted per minute from one address (default 30).
BEHIND_CLOUDFLAREtrue 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_KEYNeeded for AI Customer Support (§14) and for AI Paste of free text. The website's own enquiry email is parsed without it.

Customer install

Set in each customer's Ritzy One API .env. These carry only the public key — no signing capability.

SettingPurpose
LICENSING_ENABLEDNormally 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 / _FILEPublic 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_STOREWhere the console-managed trusted key is saved. Default <app>/data/license-public.pem; override only if you keep app data elsewhere.
LICENSE_HEARTBEAT_URLThe 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_MINHeartbeat interval, minutes (default 60).
Where the data lives
On the License Server, everything — customers, subscriptions, invoices, revocations, the issued-key audit trail, heartbeat and payment logs — is in one Firebird database, 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.

14 AI Customer Support

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.

Turning it on

Setting (License Server .env)Purpose
ANTHROPIC_API_KEYThe vendor's key. Support is off until this is set. This is the only box in the whole system that needs one.
SUPPORT_ENABLEDMaster switch (default true). Set false to take support offline without removing the key.
DOCS_BASE_URLWhere 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_DAYPer-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.

The three console pages

PageWhat it is for
SupportEvery 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 BaseYour 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 RequestsGaps 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.

How it gets better

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:

ActionEffect
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.

After uploading new manuals
Click Rebuild index on the Support page. The manual catalogue is compiled into the server build (npm run docs:catalogue in packages/license-server, then rebuild), and reloads on every boot — the button is for doing it without a restart.
What reaches you, and what does not
Each question arrives with the module and screen the user was on, their Ritzy build, and their company and user name — enough to attribute a feature request and follow up on a problem. No ledger, job, customer or payroll data is sent, and no audio: dictation is turned into text by the user's own browser. Conversations are visible to anyone with a License Server admin login, so treat the inbox as customer correspondence.

Live chat — taking a conversation over

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 seeWhat it means / what to do
A red count on Support in the menu, and (n) in the browser tab titleCustomers 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 samThe live state, shown instead of the triage status. A green dot means the customer has the conversation open right now.
Take overYou 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 AIYou 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 sendingOn 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.
AttachSend 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 callEnter 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 ticketFiles 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.

Teach the assistant what you just explained
After a live chat, Draft article works exactly as on an AI conversation — the agent's turns are included — so an explanation you typed once can become the answer every customer gets.
How it travels
Ritzy has no route into a customer's network, so nothing is pushed: the customer's panel polls through their own Ritzy server every few seconds while a person is involved (every 15 seconds otherwise), on the same licence token as the assistant. Live chat needs no ANTHROPIC_API_KEY of its own, and conversations stay scoped to one user of one customer.
What the assistant will not say
It is instructed never to invent a menu path, field or report — if the corpus does not cover it, it says so and flags the thread. On accounting it gives the general IFRS/GAAP treatment but refuses to state a jurisdiction's tax rate or filing deadline as fact, and gives no investment advice. It cannot read any customer's database.

15 Users & roles

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).

Built-in roles

RoleSees / can do
AdministratorEverything, including users and the signing key. Cannot be edited.
AccountsCustomers (read), subscriptions, invoices, payment links, saved methods, the billing run.
SalesCustomers (read), Enquiries, Proposals and the rate card.
Support AgentSupport inbox, live chat and Escalated Tickets. No customer or billing data.
Support LeadAs Support Agent, plus Knowledge Base, Feature Requests, Rebuild index and customers (read).
LicensingCustomers, licence keys, activation codes, trials, suspend/revoke.
Read onlyDashboard 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.

Adding someone

  1. User Manager → New user: username, display name (what customers see in live chat — “Sam from Ritzy Support”), email, role.
  2. Leave the password blank and a temporary one is shown once. Give it to them by a separate channel.
  3. At first sign-in they must choose their own password before anything else works. Ask them to turn on 2FA in Settings → My account.

When someone leaves, or loses their phone

SituationDo
Leaves RitzySwitch 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 passwordKey icon → a new temporary password, shown once.
Lost their authenticatorShield icon → removes 2FA; they sign in with the password and enrol again.
You cannot lock yourself out
Nobody can disable, delete or change the role of their own account, and the last active user who can manage users cannot be disabled, deleted or demoted. If the console is lost anyway, on the server run reset-admin.cmd <username> <newPassword> --make-admin: it resets the password, re-enables the account and makes it Administrator.

Audit log

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.

Upgrading
Existing logins become Administrator on the first start of this build, so nobody loses access. A brand-new server's bootstrap login (BOOTSTRAP_ADMIN_USER) must change its password at first sign-in. New passwords need at least 8 characters.

16 Database & backups

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).

Upgrading a server that used SQLite

Servers installed before 30 October 2026 kept their data in a SQLite file, data\license.db. To upgrade:

  1. Install the Firebird server on the machine, if it is not already there.
  2. Deploy the new build as usual. The deploy adds the new settings to .env; if the SYSDBA password is not masterkey, set LICENSE_FB_PASSWORD.
  3. Restart the service. On this first start the database is created and everything in 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

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:

KindWhen
DailyEvery day, automatically. The newest 14 are kept (LICENSE_BACKUP_KEEP; 0 turns it off).
ManualBack up now. Never deleted automatically.
Before restoreTaken automatically just before every restore — this is how a restore is undone.
UploadedAn .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.

Starting over on a test server

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.

Restoring

  1. Settings → Database backup, choose the backup, Restore, and type RESTORE.
  2. The backup is first restored into a scratch database to prove it is sound; a damaged backup stops here and nothing changes.
  3. The current database is backed up (Before restore), then replaced. Customer installs and the console wait for the few seconds this takes; nobody is signed out.
  4. If the replacement fails, the Before restore backup is put back automatically.

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).

Moving the License Server to a new machine
Install Firebird and the License Server on the new machine, copy .env and the signing key files from data\, start it once, then Upload the latest backup from the old machine and Restore it.

17 Troubleshooting

SymptomLikely cause / fix
“Your role does not allow this” / a page is missing from the menuThat 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 outgbak 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 / subscriptionsNo signing key yet. In the License Server, Settings → Generate key pair.
“Could not be verified” when the customer installs a keyThe 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 happeningLICENSING_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 hiddenEdit 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 effectThe install hasn't heartbeated yet (up to the interval), or LICENSE_HEARTBEAT_URL isn't set on it.
Auto-renew not runningLICENSE_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 openWebhook 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 EnquiriesHeader 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 messageFree text needs ANTHROPIC_API_KEY on this server. The website's own enquiry email works without it.
Write with AI greyed out on a proposalThe 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 acceptedIt 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 editThe 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” unavailableThe 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 RitzyThat 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 sectionWrite 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 limitRaise SUPPORT_RATE_PER_HOUR / _PER_DAY and restart, or check the Support inbox for a looping install.