Data and reports

Last updated 14 August 2026

Two versions of the same report, one without a customer condition returning every row, one narrowed with entity.id returning only the signed-in customer's rows

The data grid, the metric, the chart, the calendar and the map are all fed the same way: by a saved report. Not by an entity, not by a filter you configure on the component, and not by anything the browser asks for.

That is deliberate. A report already counts, sums, joins, filters and formats, and it is a thing you can open, run and check on its own. A second, weaker query builder inside each component would be one more place to be wrong.

The full guide to reports themselves is Reports.

The rule: the report narrows itself to the customer

The portal adds nothing to your report's SQL. If the report does not narrow itself to the signed-in customer, every customer sees every row.

The signed-in customer is available to the report as entity, so the condition goes where every other condition goes:

SELECT number, issued_on, total, status
FROM   invoices
WHERE  contact_id = {{ entity.id }}
ORDER  BY issued_on DESC
WITHOUT THE CONDITION SELECT number, total FROM invoices ORDER BY issued_on DESC Melissa signs in and sees INV-2041 · Melissa · EUR 1,240 INV-2042 · Another customer · EUR 8,900 INV-2043 · Another customer · EUR 5,200 NARROWED TO THE CUSTOMER SELECT number, total FROM invoices WHERE contact_id = {{ entity.id }} Melissa signs in and sees INV-2041 · Melissa · EUR 1,240 INV-2088 · Melissa · EUR 2,880 Hers, and nobody else's.
You have You write
a column holding the customer's id WHERE contact_id = {{ entity.id }}
a join to reach them join it, then compare against {{ entity.id }}
an account portal, rows tied to a company WHERE account_id = {{ entity.id }}
anything on the record itself {{ entity.email }}, {{ entity.company }}, any field

Nothing else in the product will raise this. A report with no such condition runs perfectly, returns rows, and draws a healthy-looking table full of other people's data. Check every report before you put it on a portal, and check it again after anybody edits it.

The safest habit is to write the condition first, before the columns, and to open the report as a test customer once the component is on the page.

Data Grid reports, not HTML reports

Reports come in two output formats, and portal components want the first:

Report format Use on a portal
Data Grid the grid, metric, chart, calendar and map: all five
HTML not offered to those components at all

An HTML report renders its own markup and returns no columns, so a chart pointed at one could only ever draw an empty picture. The pickers only offer Data Grid reports for that reason. If you want an HTML report's kind of freedom on a portal, that is what the HTML component is for, and it is better at it: it is edited where you are standing rather than in another screen.

Choosing the columns

Once a component names a report, every column picker on it offers that report's own columns. There is no naming convention to obey and nothing to rename: a map does not need columns called lat and lng, it needs you to say which of the report's columns those are.

Two consequences:

  • Answer the report setting first. The column pickers have nothing to offer until they know which report to read.
  • Renaming a column in the report can empty a setting that pointed at it. If a component stops drawing after somebody edited a report, that is the first thing to check.

What a customer's browser may ask for

Components are interactive: a grid pages and sorts, a calendar moves month, and your own markup can refresh a component with different parameters. So it is worth stating what a browser can and cannot influence:

Component May adjust
Data grid rows per page, which page, sort column, sort direction
Chart the row limit
Map the marker limit
Calendar the row limit, and the visible date range
Everything else nothing

Every one of those either narrows the answer, pages within it, or reorders it. None of them can widen what the report returned, and the limits are capped on the server, so nobody can turn a paged table into a full export by asking for a big enough page.

The ceilings

Ceiling Limit
Rows per request 200
Chart rows plotted 500
Calendar events 500
Map markers 200

These are guards, not settings: something has to stand between a report that returns a hundred thousand rows and a page trying to draw them. A component regularly hitting one of them is a sign the report should be narrower, usually by date.

For a calendar in particular, the answer is the visible range:

WHERE appointment_date BETWEEN '{{ range.from }}' AND '{{ range.to }}'

Written that way, the report returns the month the customer is looking at, and answers again each time they move.

Reports written for staff, reused on a portal

Most reports start life on a dashboard or a staff screen. Three things to look at before one goes on a portal:

  1. The customer condition. The rule above. A staff report almost never has one, because staff are meant to see everything.
  2. The links. Column formatting often builds links to staff screens, which lead a customer to the sign-in page. The markup renders correctly and the destination is still wrong.
  3. The columns. Internal ids, owner names, margins and cost prices are all perfectly ordinary in a staff report, and none of them belong in front of a customer.

A copy of the report, written for the portal, is usually cleaner than trying to make one report serve both audiences.

Checking it as a customer would see it

The builder's previews run your reports with no customer in context, which is the honest preview rather than an invented one: a report written against {{ entity.id }} previews as empty. That is not a fault, and a component that previews empty may be perfectly correct.

To see the real thing, use Preview to open the published portal and sign in as a test customer: a record on the customer entity, with an address you can receive mail at, marked active.