Final media, funnel, GA4 and support tables
One table per screen, filtered by client and pseudonymized
Upsert load into the app's landing schema
Client portal and per-client AI agent
What it solves
- Each portal screen reads a ready table with an agreed name, columns and key.
- No client sees another client's data, even when the source is shared.
- Phone numbers, emails and tax IDs reach the app pseudonymized, never raw.
Business questions it answers
Ask your agent these questions once the case is live.
What you need
- The final media and funnel tables already modeled in Nekt
- A configured destination (the app's Postgres, Supabase or an HTTP endpoint) with its own landing schema
- The registry linking each client to their accounts and their layer
What you get
- One delivery table per screen, with a declared grain and upsert key
- Loads triggered when the source finishes, with the last update date on every row
- Client isolation and pseudonymization done before the data leaves Nekt
- One table per client when each client has its own AI agent
Where to activate
Each screen reads its own table in the app database, refreshed when the source finishes.
One table per client in that client's folder, so each client's agent only sees its own data.
Warning when a delivery table exceeds its source's acceptable lag.
The modeling already exists in the other cases. The work is agreeing each screen's contract with whoever builds the app and keeping the isolation filters in place. The detailed view has the business rules, the pitfalls and the acceptance test. Or send everything straight to your agent with the Nekt MCP.
The client app does not query Nekt in real time: Nekt pushes ready data into the app's database. Without a contract per screen, any column change leaves the screen empty, an upsert with an incomplete key overwrites rows, and a missing filter on a shared source hands the client another business's data. None of this raises an error: the destination keeps running green.
How it is usually done, and the rule
Each row is a mistake that already happened in practice.
Each screen reads a consumption table with a fixed name (for example, a mart_ prefix) and explicit, typed columns. The name and the columns are the contract with the app. New columns are added; existing columns are never renamed or retyped; a change of grain becomes a new table. Source normalization lives in stg_ tables, with no business rules.
Declare the upsert key with every column of the grain. On the ads screen, the key is date, client, platform, ad set and ad: the ad set is included because in Google the same ad can sit in more than one ad group. Run a uniqueness check on the key before each load.
The destination writes to a landing schema (staging or landing). The app's triggers and access rules (RLS) handle deduplication, derivation and exposure from there. Writing straight to the public schema bypasses that logic and leaves raw data visible to the front end.
A shared source (a Google Business Profile account with many locations, a Business Manager with many ad accounts) needs a filter by the client's official identifier (location_id, account_id from the registry) inside the delivery transformation itself. A forgotten filter raises no error: it delivers dozens of businesses' data to one client. When each client has its own AI agent, write one table per client in that client's folder, so the agent only sees its own dataset.
Pseudonymization happens in Nekt, not in the app. A phone number becomes a hash of the normalized number (digits only) plus the last 4 digits; email and tax ID become hashes; addresses and fields the dashboard does not use are not sent. If a screen needs the raw value, the discussion is about app permissions, not about the query.
Link client and layer through an identity column in the client registry (for example, client_slug or the Nekt layer recorded in the portal), with at most one client per layer. Layer names cannot be renamed and drift from the client's commercial name.
The delivery transformation fires when the run of the source it reads finishes, so it never runs before extraction is done. A fixed schedule only where the destination has its own window. A manual trigger is not a live pipeline and must be flagged as such.
Every delivery table carries the load timestamp and the latest data date. The copy of the portal's client registry in Nekt can lag up to 24 hours: before calling a mismatch between portal and Nekt, check the source's last run.
Metric columns use the same names as the portfolio media table (spend, clicks, leads, conversions), so the app can reuse its mapping. Each platform fills its own columns and leaves the others null, never zero. Values are already in reais, converted before delivery.
The ad image comes from adimages.permalink_url in Meta, described as a permanent link. Other thumbnail links expire. Flag on each row whether the thumbnail is permanent, so the app knows when to fetch again.
From warehouse to screen
Layers that separate business rules from what the app reads.
Definitions
Pitfalls that already happened
A column was renamed in the delivery table. The destination kept delivering and the app started reading a null field, with no error at all.
Treat the table as a contract: add columns, never rename.
The agency's Google Business Profile account covered dozens of locations. Without the location_id filter, one client's dashboard would show every location's reviews.
Filter by the client's official id inside the delivery transformation.
In Google, the same ad appeared in two ad groups. With the key missing the group, the second row erased the first.
Include the ad set or ad group in the upsert key.
Edits made in the portal after the daily extraction only showed up in Nekt the next day, and client count and MRR diverged for a day.
Check the source's last run before calling a mismatch.
One client's dashboard depended on a connector registered in the agency's own layer. When it failed, the investigation only looked at the client's layer.
Document the source layer of each delivery table.
Two active destinations delivered the same data to the same app, one newer than the other. Changing the wrong one had no effect.
One destination per delivery table, with the old one switched off.
Acceptance test
The case is done when every item passes.
- For one client and one closed month, spend summed in the delivery table equals the portfolio media table (zero difference).
- The upsert key does not repeat in any row of the delivery table.
- Each destination receives exactly one distinct client, or only the clients authorized for that app.
- No delivered phone number has more than 4 readable digits.
- The last load date of each table is within its source's acceptable lag.
Agent package
Paste into Claude, ChatGPT or any client with the Nekt MCP connected.
Client portal - The portal does not read Nekt in real time. Nekt delivers consumption tables (mart_<screen>) into the landing schema of the app database, by upsert. - Each delivery table is a contract: fixed name, columns, grain and key. Do not rename columns; a grain change is a new table. - Upsert key for the ads screen: date, client, platform, adset_id, ad_id. - The destination never writes to the app's public schema. - Every delivery table is filtered by the client's official id (account_id, location_id) inside the transformation. A shared source without a filter is a leak. - Phone numbers leave as a hash of the normalized number plus the last 4 digits. Email and tax ID leave as hashes. Addresses do not leave. - The client is linked to the layer by the registry's identity column, never by name. - To answer whether the portal is up to date, read the last load date and the latest data date of the delivery table. - One metric name across platforms; null where the platform does not measure it. Client exceptions: - <client>: destination <destination>, schema <schema>, screens <list of mart_ tables>. - <client>: reviews filtered by location_id <id> from the agency's shared account.
Build the "Client portal" case in my Nekt workspace following the rules at https://academy.nekt.com/en/industrias/agencias/portal-do-cliente.md. 1) Find the source tables and confirm the columns the rules use. 2) Create a transformation at the stated grain applying every rule. 3) Run the acceptance test and show me the result of each item. 4) Create the case's context document in the workspace, leaving my clients' exceptions open for me to fill in.
Variations
App without a reachable database: send the table in batches (e.g. 1,000 rows per call), with the token kept in Nekt's secrets vault, never in the code.
To reuse a ready Looker dashboard, deliver the table with the same field names as the connector that used to feed the template.
The same pattern works for screens beyond marketing, such as reviews, support or renewals, always with one table per screen.