- A property manager asks a helpful question: “How much did we spend on plumbing across all properties last quarter?”
- The assistant returns one number.
- No customer names appear. No invoice is displayed.
- Conversational analytics changes the shape of access control.
The client problem
A property manager asks a helpful question: “How much did we spend on plumbing across all properties last quarter?”
The assistant returns one number.
No customer names appear. No invoice is displayed. No order row is exposed. Yet the number may still be a data leak if “all properties” included locations the manager is not allowed to see.
Conversational analytics changes the shape of access control. Traditional screens protect resource lists and detail pages. An assistant can return totals, averages, maxima, vendor concentration and counts. Every aggregate reveals facts about the rows beneath it.
For a property-procurement marketplace built with Medusa, we designed the purchase-history assistant so access scope is resolved before analytics. The model may interpret the question and propose filters, but it cannot grant properties. The backend derives an organisation and property perimeter from the authenticated member, intersects the plan with that perimeter and only then computes rows or aggregates.
Natural language makes data easier to question. It does not make permissions conversational.
Aggregates are still protected data
Teams often secure record endpoints and overlook analytical answers.
Suppose a supervisor may manage two properties inside a portfolio of twenty. They cannot open orders from the other eighteen. But an unrestricted query for total spend, highest unit price or unique vendors can reveal the activity of the whole organisation without returning one forbidden order.
The same problem appears through comparisons. “Which property spent the most?” discloses a ranking. “Did any site pay more than $500?” reveals whether a protected transaction exists. Even a zero can expose inactivity.
We therefore apply the same property perimeter to every analytical operation: row listing, count, sum, average, breakdown, anomaly and cadence.
Access is not a final redaction pass over prose. It shapes the SQL population from which the answer is calculated.
This is the central rule: if the caller cannot access a row, that row cannot influence a number the caller receives.
Derive organisation from authentication, not the question
The assistant never asks the model which organisation the user belongs to.
The Store API enriches the authenticated request with customer organisation context. The purchase-history scope receives that organisation ID from the server-side context, not from a free-form request field or a phrase in the conversation.
Every analytical base query begins with organization_id = ?. Product, vendor, property and allocation joins are built from orders already connected to that organisation.
A user can mention another company, paste a property identifier or ask for “the entire platform”. Those words may affect intent classification, but they cannot replace the authenticated organisation boundary.
This prevents a familiar multi-tenant mistake: trusting a client-supplied tenant ID because the route is already authenticated. Authentication tells us who is speaking; server-side context tells us which organisation governs the request.
The model receives analytical vocabulary. It never receives authority to select tenancy.
Resolve accessible properties by role and relationship
Organisation membership alone is not enough.
A master user can operate across the organisation's active properties. Other roles depend on explicit customer-property relationships. Supervisor-like roles require a manage-level link. Buyer-side members use their assigned property links.
The resolver queries those relationships inside the same organisation and excludes deleted properties or links. The result is a concrete list of property IDs the request may touch.
This list is calculated before the language plan executes. It is not inferred from a role label in the model prompt and not cached as a universal organisation-wide scope.
When the member has no accessible properties, the analytics path becomes deterministically empty. It does not fall back to the whole organisation because an empty list might have meant “no filter”.
The distinction between null and an empty list is security-sensitive: unrestricted master scope and no accessible properties must never collapse into the same representation.
“All accessible” means all the caller's properties
Natural questions often include broad language: “globally”, “across the portfolio”, “all properties” or “for the whole account”.
The planner represents that as all_accessible, not all. For a master, the base organisation boundary supplies the portfolio. For other roles, the resolved accessible-property list remains in the SQL scope.
This creates a user-friendly semantic contract. A manager can ask for “all properties” and receive all properties they manage without needing to list them. The phrase cannot widen their permissions to the organisation's other sites.
The current-property mode works in the opposite direction. When the application has an active property, the scope can narrow to that property. Switching to all-accessible deliberately broadens only within the caller's existing perimeter.
The answer can therefore explain its scope in business language: current property, or all properties this account may access.
Intersect property filters instead of replacing the perimeter
The model plan may contain specific property IDs or names.
Those filters are useful when a user asks about one location. They are applied in addition to the resolved access scope, not instead of it. A supplied list cannot replace the accessible list with a broader one.
At SQL level, the base scope first restricts property_id to accessible IDs. Planned property IDs add another ANY predicate. The result is their intersection.
Property names are also filters over already scoped rows. A name matching a location in another organisation has nothing to match because those rows never entered the scoped population.
This defence is stronger than validating only the model's selected properties. Even if the planner emits an unexpected or stale identifier, the database query still cannot expand beyond the authenticated perimeter.
Permissions and analytical intent remain two independent clauses that must both pass.
Keep current-table context from widening access
The assistant can inherit filters from the Purchase History screen.
If the user asks “what is the total in this view?”, the application may pass current property, GL, unit, status, category, manufacturer, city, state, vendor and date filters into the plan. This reduces repetition and keeps the conversational answer aligned with what the user sees.
Those context values remain subject to the same accessible-property scope. They cannot grant a property merely because a URL contains its identifier.
The inheritance rule is also selective. Current-table filters are used only when the latest question explicitly refers to that view or clearly continues it. A new product, vendor or property subject removes stale context values unless the question mentions them.
This protects correctness as well as privacy. A hidden leftover property filter can make a safe answer misleading, even when it does not cross a tenant boundary.
The interpreted scope should remain visible to the user so they can understand which screen context the answer retained.
Run every metric inside the same scoped CTE
Access rules become fragile when each metric implements its own query.
We built one scoped purchase-history common-table expression that combines the organisation boundary, accessible properties and allowed filters. Rows, metrics, breakdowns and examples all derive from that same scoped relation.
Total spend, average price, unique entities, vendor shares, anomaly counts and reorder cadence therefore cannot accidentally use a broader table than the visible example rows.
This also reduces policy drift. Adding a new aggregation does not require an engineer to remember every role clause again. The metric receives only the population the shared scope admitted.
The architecture is a repeatable pattern that places access before computation and gives tests one canonical boundary to protect; future analytical paths must continue to use it.
One governed analytical surface is safer than many bespoke report queries with slightly different interpretations of “my properties”.
Do not let the model calculate or narrate around the boundary
The language model produces a constrained plan. It does not execute SQL and it does not receive unrestricted rows from which to calculate its own answer.
The backend validates filters and aggregations, resolves catalogue terms where needed and runs deterministic analytics against the scoped relation. The final user-facing answer is assembled from those computed values.
This avoids a second leak path: securely querying approved rows, then sending a broader data dump to a model for summarisation. The model only needs the bounded plan context and the permitted result needed for the answer.
Unsupported questions receive deterministic guidance about the available purchase-history dimensions. Clarifications can ask for product, vendor, property, unit, GL, status or date without implying the assistant has access to everything named.
The model improves the interface. The backend owns both permission and arithmetic.
Return empty safely when the perimeter is empty
Empty access deserves explicit handling.
In query builders, an empty array is sometimes treated as “no filter”, which can accidentally expose every row. The purchase-history scope detects an empty accessible-property list and produces an always-empty query instead.
This matters during onboarding, role changes and deleted assignments. A user may remain authenticated while temporarily having no property relationships. The safe result is no purchase history, not organisation-wide history.
The assistant can explain that it can answer only from accessible purchase history. It does not need to expose whether other properties have data or which assignments are missing.
Failing closed here is not an unfriendly edge case. It is what gives administrators confidence that removing access has immediate analytical effect.
No properties means no contributing rows, no aggregates and no examples.
Test roles, not only happy-path prompts
Conversational evaluation often focuses on whether the model understands the question.
Security evaluation must also vary the caller. The relevant contracts include master portfolio access, supervisor manage links, buyer assignments, active-property narrowing, all-accessible language, supplied property filters and an empty accessible set.
Tests should verify both visible rows and aggregate values. It is possible for examples to be scoped correctly while a count or total still uses an organisation-wide query.
The fixed repository includes focused analytics contracts and role-aware scope code. That supports the implemented pattern without claiming that every future route is automatically safe.
As the assistant gains new metrics, the review question remains simple: does this operation consume the canonical scoped relation, or did it build a new path around it?
Access control is a maintained product property, not a one-time penetration-test checkbox.
Why Medusa was the right foundation
Medusa supplied authenticated customer APIs and the order lifecycle. The project extended that foundation with organisations, properties, roles, allocations and purchase-history analytics.
Because the assistant lives inside the commerce backend, it can reuse the same authenticated context and domain relationships that govern normal Store routes. It does not operate as a separate chatbot with its own copied permission table.
We added the scoped SQL projection and constrained planning layer needed for conversational questions. The commerce engine remains authoritative for which orders belong to the organisation and which customer-property relationships are active.
This turns AI into another interface over governed commerce data rather than a privileged side door into the database.
The architecture choice serves the client's organisational model: easier access to insight without broader access to information.
A practical analytics-perimeter checklist
Before releasing conversational analytics, ask:
- Is organisation identity derived from authenticated server context?
- How are accessible properties resolved for each role?
- Does a supervisor require the correct relationship level?
- What does an empty accessible set mean in SQL?
- Does “all properties” mean all accessible properties?
- Can a model-proposed property ID expand the perimeter?
- Are property-name filters applied after organisation scoping?
- Can current-table context grant access?
- Are stale context filters removed for a new subject?
- Do rows and aggregates share one scoped relation?
- Can inaccessible rows influence a count, average or ranking?
- Does the backend compute the answer deterministically?
- What result is shown when no properties are accessible?
- Are role changes reflected without model retraining?
- Do tests compare aggregate scope as well as example rows?
These questions matter more than how natural the assistant sounds.
The broader lesson
An analytical answer is a view over data, even when it is only one sentence.
For this marketplace, every purchase-history question begins with authenticated organisation context and a resolved property perimeter. “All accessible” stays inside that perimeter. Planned property filters can narrow it but never replace it. The database builds one scoped population before calculating rows, totals, averages, anomalies or breakdowns.
The language model does not execute the query, choose the tenant or calculate around the access boundary.
That is how conversational commerce can be both useful and governable: make questions flexible, keep authority deterministic and ensure no forbidden row contributes even one digit to the answer.
