---
id: saas/dados-entregues-aos-clientes
industry: SaaS and technology
usage: widely used
effort: medium
grain: Tenant x day or month, and tenant x customer entity (order, campaign, item, call, device)
---

# Data delivered to the SaaS's customers

ROI reports, a portal and extracts for every customer of your SaaS, all coming from a single model, isolated per customer and with numbers the customer can verify.

## Executive summary

**For:** SaaS companies that need to prove value to each customer or hand them their own data.

**What it solves**

- Every customer gets the same value calculation, without a manual query per request.
- No customer sees another customer's data, or any integration credential.
- The delivered number says whether it is measured, estimated or missing, and the customer can verify it.

**Why it matters:** The value report is what secures the renewal: it is where the customer sees attributed revenue, savings or the impact of a feature. When each customer becomes a copy of the transformation, numbers diverge between customers and every fix has to be made hundreds of times. A leak between customers ends contracts.

**Business questions it answers**

- How much revenue did our tool generate for each customer last month?
- Which customers saw results drop from the previous month and need a CS conversation?
- How much has the customer saved since starting to use the automation?
- Do products with our feature sell more than those without it?
- Which customers have a stalled integration and therefore show no results?
- How long does it take to put a new customer on the portal?

**What you need**

- Product database connected to Nekt, with the tenant id in every table or reachable by key
- A per-tenant configuration table: id, destination layer, start date and active sources
- The value rule agreed with customers: attribution window, baseline and rate

**What you get**

- Per-tenant tables, isolated by layer and with no sensitive columns
- A monthly value report with the closed month, comparison and alerts
- A portal or extract to the customer's database fed by the same model
- An AI agent that answers per customer without mixing data

## Detailed

### The problem

SaaS customers ask for their own data and proof of value, and each request turns into a query on the production database or a copy of the transformation with the customer's id hard-coded. Some workspaces have hundreds of near-identical copies, and a rule change has to be repeated in all of them. Meanwhile, the value metric has details that change the number: attribution window, timestamps truncated by the integration, days with no measurement and missing rates.

### Business rules

1. **One parameterized model, not one copy per customer.** Write the transformation once with the tenant id as a column and read the tenant list from a configuration table (id, display name, destination layer, start date, active sources, parameters such as window and rate). A new customer is one row in that table. Copies are only justified when the customer has its own source that doesn't exist for the others, and even then they call the same calculation. (Avoid: Duplicating the transformation and swapping the customer's literal id in each copy.)
2. **The layer is the permission boundary.** On Nekt, permissions are granted per layer. Deliver each customer in its own layer (or its own destination) and never in a shared table filtered only at read time. The consolidated table with every tenant stays in an internal layer, visible only to the SaaS team. (Avoid: A single table with every customer and a filter on the dashboard.)
3. **Filter by tenant and propagate through keys.** Tables that have the tenant id are filtered directly. Child tables without that column (items, messages, insights) come in through the foreign key of their parent table; reference tables come in with only the rows the tenant's records use. Check that every copied table has a path to the tenant. (Avoid: Copying entire child tables because they have no tenant column.)
4. **Remove sensitive columns before delivering.** Keep a closed list of columns that never leave: client_secret, access_token, refresh_token, passwords, API keys and integration credentials. Exclude them from every delivered table, including those from standard connectors, such as the Stripe payment intent's client_secret. (Avoid: Copying the whole table from the product database into the customer's layer.)
5. **Attribution with a declared window.** Revenue attributed to a message, campaign or automation counts only orders from the same end customer placed after the send and within the agreed window (one real case uses 3 days). Each order is attributed to a single send, the most recent one before it. Amounts in cents are divided by 100 once. (Avoid: Summing every order from the contact in the month as the tool's revenue.)
6. **Detect truncated timestamps per tenant.** Some order integrations send only the date, with time 00:00:00. For each tenant, look at the last three orders: if all have zeroed hour, minute and second, treat the field as a date and compare the window in days; otherwise, as a timestamp. Store the decision in the tenant's configuration. (Avoid: Comparing an order at midnight with a message sent at 3 p.m. and losing same-day attribution.)
7. **No data is null, with the reason.** If an input is missing (rate not registered, meter offline, stalled integration), the result is null with a reason column, never zero. Distinguish a data error from a deliberate decision: a customer who only contracted kWh metering has null savings in reais and no alert, not an incomplete-rate alert. (Avoid: Writing zero savings when there is no rate.)
8. **Estimated shows as estimated.** Days with no reading can be filled with the average for the same weekday in the nearest real period, but the row carries a flag (measured or projected). Never use a fixed fill value. The savings baseline is the per-weekday average over the monitoring window before the automation; if operations change (for example, the number of devices), open a new baseline regime with a cutoff date. (Avoid: Filling gaps with a fixed value and delivering it as real data.)
9. **With and without the feature, from the same catalog.** To prove a feature's impact, split the customer's items into with and without it based on the list the customer maintains, and compare views, cart, purchase and revenue over the same period. The match is by item id; with no id, by a name that starts with the registered name, keeping a single match, the most specific one (longest name). (Avoid: Matching by name with contains and counting the same item in two groups.)
10. **Read the application's raw tables and document the misleading ones.** When the product writes its own derived tables (prefixes such as dw_ or dwt_) alongside the application tables, use the application tables, which are the source of truth. Record in the context document the tables whose names mislead (for example, a table called credit that stores charges), filter soft deletes (deletedAt null) and drop from the report any metric whose status is never filled in at the source. (Avoid: Building the report from derived tables and trusting the table name.)

### How each delivered number is flagged

Every row delivered to the customer carries the value type, so the portal and the agent can explain the number.

- **Real reading or record in the period** → Measured (`tipo_valor = medido`)
- **Gap filled with the average for the same weekday** → Projected (`tipo_valor = projetado`)
- **Input missing due to an error: rate, meter, integration** → No data, with alert (`null value, reason filled`)
- **Input missing by contract choice** → Not applicable (`null value, no alert`)
- **Order outside the attribution window** → Not attributed (`excluded from attributed revenue`)
- **Integration with no new order for N days** → Stalled integration (`non-comparable result`)

### Definitions

| Metric | Definition |
|---|---|
| Tenant | a customer of the SaaS and the unit of isolation; one row in the configuration table with id, destination layer, start date, sources and parameters |
| Attributed revenue | sum of the tenant's order amounts placed after a send and within the window (e.g. 3 days), each order attributed to the most recent send, in reais, by tenant and month |
| Delivery rate | messages with status sent, delivered or read / messages dispatched, by campaign and channel |
| Connection rate | calls with status connected / completed calls (excludes preparing and in progress), by tenant and period |
| Influence rate | influenced delivered orders / reachable base (contacts with a previous order or conversation, with no campaign opt-out), by tenant and month |
| Savings | (per-weekday baseline minus actual consumption) x time-of-use rate, by tenant and day; null when the rate is missing |
| Monthly value report | the previous closed month against the month before it, with percentage delta and alerts, one row per tenant |
| Feature impact | views, cart, purchases and revenue of items with the feature against those without, in the same period and tenant |

### Pitfalls

- **Hundreds of copies.** One transformation per customer grew to hundreds of versions; a rule fix left customers with different numbers for the same calculation. How to avoid: Parameterize by tenant and migrate the copies to the single model.
- **Connector prefix hard-coded.** When each connected source generates tables with the connector id in the name, the id changes between workspaces and when the source is reconnected; queries with a hard-coded id break. How to avoid: Resolve the current prefix by listing tables before querying, and never write the id into the context document.
- **History wiped by a full sync.** A full sync from the source deleted months of raw data that no longer existed in the API, and the value report lost its history. How to avoid: Keep a frozen backup of the history already delivered and reprocess on top of it.
- **Customer spreadsheet with text in numbers.** Registration spreadsheets came with decimal commas, thousands separators, currency symbols, empty cells and errors such as #DIV/0!. How to avoid: Convert with a safe try and turn errors and blanks into null, never zero.
- **Dates in four formats.** The same date field came as year-month-day, month/day/year, day/month/year and spelled-out month, depending on the tenant. How to avoid: Try the formats in a fixed order, record the format per tenant and flag what didn't convert.
- **Different calendar between engines.** Weekdays numbered differently in Spark and Athena shifted the baseline to the wrong day, and movable holidays such as Easter were left out. How to avoid: Use a single calendar with business days and national and movable holidays, generated once.

### Acceptance test

- [ ] For one tenant and one closed month, attributed revenue matches the product's own dashboard (difference below 1%) and no order is attributed to two sends.
- [ ] In each customer layer there is exactly one distinct tenant id across all tables.
- [ ] No delivered table contains columns from the forbidden list (client_secret, access_token and the like).
- [ ] The number of rows flagged as projected equals the number of days with no reading, and there is no zero savings on a row with no rate.
- [ ] Adding a test tenant takes only one configuration row and produces its report on the next run.

### Context document

```text
Data delivered to the SaaS's customers
- Tenant = a customer of the SaaS. The list of tenants and their parameters (destination layer, start date, attribution window, rate, date format) is in the tenant configuration table.
- Every question about a customer filters a single tenant. Never mix tenants in an answer for one customer.
- Each customer's layer is the permission boundary. The consolidated table is internal.
- Never expose client_secret, access_token, refresh_token, passwords or API keys.
- Attributed revenue: orders after the send and within the tenant's window (default 3 days), each order on the most recent send. Amounts in cents divided by 100.
- A tenant with truncated order timestamps (00:00:00) compares the window by date.
- A null value means no data; read the reason column. Null is not zero.
- Rows with tipo_valor = projetado are estimated from the average for the same weekday. Say they are estimated.
- Monthly report: the previous closed month against the month before it.
- Use the application tables, not the derived ones prefixed dw_ or dwt_. Filter out records with deletedAt filled in.
- If the connector generates tables with an id prefix, find the prefix by listing the tables before querying.
Per-client exceptions:
- <client>: attribution window of <n> days instead of 3.
- <client>: contracted metering only; savings in reais stay null and raise no alert.
- <client>: baseline changed on <date> due to an operational change; before and after are separate regimes.
```

### Variations

- **Media and website per customer:** When the connector is one account per source, each customer comes in as one more source in the same model, with the agreed canonical conversion (e.g. a sign-up event in GA4).
- **Raw extract for the customer:** Instead of metrics, the filtered copy of the tenant's tables to their database, with the same key propagation and forbidden-columns list.
- **Metrics per internal department:** The same design works for per-department dashboards: one layer per area, with the attainment calculation in the pipeline and not in the agent.
