Data Scoping
Widget visibility controls rendering — but data scoping ensures that even if a widget renders, it only receives data the user is authorized to see. Data scoping happens server-side in the data services that App Studio widgets call, not in the UI layer.
Why Data Scoping Is Necessary
Hiding a widget does not prevent an API call from being made. If a widget makes a data request, that request returns whatever the server sends — regardless of UI visibility rules. Data security must be enforced at the server.
// WITHOUT data scoping:
// A viewer might modify the URL, reload the page,
// and the DataGrid would load all records — even from a "hidden" widget.
// WITH data scoping (correct approach):
// The data service enforces tenant + role filters on every query.
// Even if the request is made, only authorized records are returned.
How Data Scoping Works in App Studio
App Studio widgets load data through data services configured with the widget. The data service is responsible for applying role-based filters. App Studio passes the user's context (including roles) with every data request:
The DataGrid, Form, or other data-bound widget calls its configured data source.
App Studio includes the user's Passport JWT in the Authorization header of the API call. The server extracts tenantId, userId, and roles from the token.
The data service (e.g., a Flow Studio workflow or a backend API) applies tenant isolation and role-based data filters before returning records.
Only authorized records are included in the response. The widget displays what it receives.
Passing context to Data Sources
Use context.* tokens in widget data source params to pass the user's identity to backend filters:
// DataGrid — load only leads assigned to the current user (sales rep scope)
{
"widgetId": "my-leads-grid",
"type": "DataGrid",
"config": {
"dataSource": "GetLeads",
"params": {
"assignedUserId": "{{ context.userId }}", // Server filters by this
"tenantId": "{{ context.tenantId }}"
}
}
}
// For managers — load leads for their team
// The backend uses context.roles to determine if the user can see all leads
{
"dataSource": "GetLeads",
"params": {
"requestingUserId": "{{ context.userId }}",
"requestingUserRoles": "{{ context.roles.join(',') }}"
}
}
Tenant Isolation
Tenant isolation is enforced at the data layer — always. The tenantId from the JWT is used by all data services to scope queries. App Studio never mixes data across tenants regardless of widget configuration.
// The platform guarantees: all data requests carry tenantId from the JWT.
// Data services MUST NOT use tenantId from query params alone —
// always validate against the JWT claim.
//
// This is enforced by the IAppStudioService architecture —
// the service layer always sources tenantId from the authenticated context.
Role-Based Column Visibility + Data Scoping Together
// Combining UI hiding (column visibility) with data scoping (server-side):
// Even if someone bypasses the column visibility in the UI,
// the server won't return the salary field for non-HR users.
// Widget config — hide salary column in the UI for non-HR
{
"field": "salary",
"header": "Salary",
"visibilityExpression": "{{ context.roles.includes('hr') }}"
}
// Server-side data service — exclude salary from non-HR responses
// (This lives in the Flow workflow or backend API, not in App Studio config)