Modules (NXM)
How the customer portal is built
The parts behind a customer portal, what each of them knows and what it never sees, and the path of one request from the visitor's browser to the module service and back.
A holiday-home agency runs its booking site on nexxus. Guests search on a map, open a house, see the free dates and the price for their stay, and send a request that lands in the agency’s inbox. Neither nexxus nor the portal app contains any code about holiday homes. What a listing is, which of its fields a guest may see and how a price is worked out all belong to the agency’s package. Another package publishes its own records the same way, without a change to nexxus.

This page explains the parts and how they work together. How to write the portal section of a manifest is on Customer portal.
Four parts, one job each
Section titled “Four parts, one job each”| Part | What it does | What it never does |
|---|---|---|
| nexxus | Finds the tenant by the web address, keeps the records, decides what is public, serves images and the map, passes endpoint calls on to the service | Run code of a package. Let anyone sign in on a portal address |
| The package | Declares in its manifest the entities, the public fields and settings, the pages and the endpoints | Bring a web server, or code that runs inside nexxus |
| The module service | Works out what needs logic: free dates, a price, a booking request, a message to the host | Keep data or credentials of its own. It reads and writes through the record API of nexxus, with a token that expires within minutes |
| The portal app | Renders the pages the package declares, first on the server, then in the browser | Know about an industry. Store anything on the visitor’s device |
The same records serve two audiences. The agency’s staff work with them in nexxus, in the package’s lists, calendars and map views, with their rights and the AI Assistant. Guests see the public part of them in the portal. There is no copy and no export in between: a house the agency switches to inactive leaves the site within a minute, because answers of the portal API may be kept for 60 seconds.
One request, end to end
Section titled “One request, end to end”A guest opens a listing page and chooses dates:
- 1GET /stays/villa-silvertreeBrowser → Portal app
- 2Site, page definition and the listing, with the visitor's hostPortal app → nexxus
- 3Host to tenant, public record, public fields onlynexxus
- 4The finished pagePortal app → Browser
- 5GET /public/portal/<module>/endpoints/quote?record=12&check_in=...Browser → nexxus
- 6Rate limit, declared parameters, record is publicnexxus
- 7POST /portal/quote with an invocation token (60 s)nexxus → Module service
- 8Exchange the token, read the records the price needsModule service → nexxus
- 9The quote as JSONModule service → nexxus
- 10The quote, refusals cleaned upnexxus → Browser
- The first answer is a finished page. The portal app asks nexxus for the site, the page definition and the record, and sends the visitor’s host along (
X-Forwarded-Host), because nexxus picks the tenant by the host. Search engines read the page as it is. In the browser the app takes over without asking for the same data again. - Everything after that goes from the browser to
/public/portal/...on the same address: other pages, the next page of results, the map area, prices and forms. - Endpoints run through nexxus. nexxus checks the call before the service sees it and checks the service’s answer before the visitor sees it. A service that is down or takes longer than 10 seconds becomes
503for the visitor, with no text of the service in it.
One address, one tenant
Section titled “One address, one tenant”A tenant’s domains have a role. Its own domains carry the app and the sign-in; its portal domains carry the portal and nothing else. A superadmin sets both under AdministrationCompanyTenants.
- The tenant comes from the host alone, never from a parameter, a cookie or a header that the reverse proxy did not set. nexxus takes
X-Forwarded-Hostonly from proxies on private addresses. - A portal host answers two paths: the portal API under
/public/portal/and the map under/public/map/. The sign-in, the API of the app, the live channel and everything else answer404there, as if they did not exist. On every other host the portal API answers404. - The portal app renders pages only for the hosts it is configured for, so a forged
Hostheader cannot make it fetch data from another address. - Pages on other sites cannot read the portal API in a browser: it allows cross-origin calls only from the tenant’s own portal hosts, and never with cookies.
What leaves nexxus, and who checks it
Section titled “What leaves nexxus, and who checks it”The portal only ever gets what the package declared as public, and nexxus checks that at four points.
- At upload. The package check refuses a portal that publishes a contact, a person, a JSON field or a field that does not exist, filters on a field that is not indexed, or names a page, endpoint or service that is missing. See Rules the package check enforces.
- At install. The administrator consents to
portal:public, shown as one sentence that nexxus builds from the manifest: which records, which fields, that images and a rounded location go along, which service visitors can call. An update that publishes more asks again. - On every answer. nexxus returns only the records the entity’s public filter selects and only the fields of
public.<entity>.fields. It drops non-publishable types again, even for a manifest stored before a rule existed. It rounds a location to two decimals (about 1 km) unless the manifest saysexact. Images get an address signed for tenant, installation, record and document; each request checks again that the record is public and still names the image, and only JPEG, PNG, WebP, GIF and AVIF go out. - On every endpoint call. nexxus counts the call against the rate limit first, then passes on only the declared parameters (at most 200 characters each), checks that
recordis a public record, refuses a body over 32 KB and checks the form field by field. A filled honeypot field gets a friendly answer and goes nowhere. From the service’s answer the visitor gets at most 64 KB of JSON, or a refusal with its texts shortened to plain text.
The module service checks everything again, because the call comes from the internet.
The module service holds nothing
Section titled “The module service holds nothing”The service is a separate process. nexxus calls it with an invocation token whose trigger is portal:<endpoint>, so a token for the price cannot send a booking request. A portal call has no person behind it: when the service calls back, it acts as the installation’s service account, with the scopes the administrator consented to. How the tokens work is on Module services.
What the service writes appears in nexxus like any other record. In the holiday-home package a booking request becomes a guest (found by e-mail address, otherwise new), a reservation with the status inquiry and the guest’s message in the agency’s inbox. By default the service drafts a reply from the listing’s own fields and holds back Wi-Fi and arrival details until the stay is booked. Staff read the draft, change it if needed and approve it.
The map comes from nexxus
Section titled “The map comes from nexxus”The map in the portal and the map views in nexxus use the same component and the same source: one PMTiles archive with OpenStreetMap data, plus its fonts and symbols, served by nexxus under /public/map/. The browser reads the archive in byte ranges, so nexxus needs no tile server. No request goes from the visitor’s browser to a map provider.
The map belongs to the installation, not to a tenant. An archive can hold the world at low zoom levels and the regions an installation serves in full detail; outside those regions the map stops zooming in instead of showing empty tiles.
Address search and geocodeFrom of package fields run on the server only. By default they ask the public Nominatim server of the OpenStreetMap Foundation, at most once a second as its usage policy asks, and keep each answer for 180 days (an address without a hit for 7 days).
Nothing on the visitor’s device
Section titled “Nothing on the visitor’s device”The portal app sets no cookies and uses no local storage, session storage or IndexedDB. Pages, data, images, map tiles and fonts all come from the portal’s own address. Search as I move the map is held in memory for the visit. That is why a site built on it needs no cookie banner. Keep it that way when you build on the portal, and say so in the site’s privacy notice, as the holiday-home package does.