diff --git a/docs-mintlify/docs.json b/docs-mintlify/docs.json
index 469603d5934bf..432e0600e88c7 100644
--- a/docs-mintlify/docs.json
+++ b/docs-mintlify/docs.json
@@ -573,7 +573,8 @@
"recipes/data-modeling/funnels",
"recipes/data-modeling/cohort-retention",
"recipes/data-modeling/xirr",
- "recipes/data-modeling/dbt"
+ "recipes/data-modeling/dbt",
+ "recipes/data-modeling/layered-access-policies"
]
}
]
diff --git a/docs-mintlify/recipes/data-modeling/layered-access-policies.mdx b/docs-mintlify/recipes/data-modeling/layered-access-policies.mdx
new file mode 100644
index 0000000000000..5859c5b63d673
--- /dev/null
+++ b/docs-mintlify/recipes/data-modeling/layered-access-policies.mdx
@@ -0,0 +1,209 @@
+---
+title: Layered access policies with overlapping groups
+description: Combine a base group for member access with regional groups that add row-level filters, avoiding the OR-semantics pitfall where a permissive policy overrides restrictive ones.
+---
+
+## Use case
+
+You manage access through an identity provider (e.g., Okta) where users belong
+to a base group like `viewer` and optionally a regional group like
+`viewer_north_america`. You want the base group to grant member-level access
+while regional groups layer on row-level filters that restrict which rows a user
+can see.
+
+The challenge: when a user belongs to both groups, Cube combines matching
+policies with **OR semantics** for row-level filters. A base policy with _no_
+row filter is maximally permissive, so it effectively overrides the regional
+filter. The user sees all rows instead of only their region.
+
+## Data modeling
+
+Consider an `orders` cube with a `country` dimension:
+
+```yaml
+cubes:
+ - name: orders
+ sql: >
+ SELECT 1 AS id, 100 AS amount, 'US' AS country UNION ALL
+ SELECT 2, 200, 'CA' UNION ALL
+ SELECT 3, 300, 'MX' UNION ALL
+ SELECT 4, 400, 'DE' UNION ALL
+ SELECT 5, 500, 'JP'
+
+ dimensions:
+ - name: id
+ sql: "{CUBE}.id"
+ type: number
+ primary_key: true
+
+ - name: country
+ sql: "{CUBE}.country"
+ type: string
+
+ measures:
+ - name: count
+ type: count
+
+ - name: total_amount
+ sql: "{CUBE}.amount"
+ type: sum
+```
+
+And a view that exposes it:
+
+```yaml
+views:
+ - name: orders_view
+ cubes:
+ - join_path: orders
+ includes: "*"
+```
+
+### The problem
+
+A naive approach assigns member access to the base group and a row filter to the
+regional group:
+
+```yaml
+views:
+ - name: orders_view
+ cubes:
+ - join_path: orders
+ includes: "*"
+
+ access_policy:
+ - group: viewer
+ member_level:
+ includes: "*"
+
+ - group: viewer_north_america
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["US", "CA", "MX"]
+```
+
+A user in both `viewer` and `viewer_north_america` matches both policies. The
+`viewer` policy has no row filter (all rows allowed), and the
+`viewer_north_america` policy filters to North American countries. Because
+row-level filters from multiple matching policies combine with OR, the
+permissive `viewer` policy wins and the user sees all rows.
+
+### The solution
+
+Add a row-level filter to the base group that matches **no rows**. This makes the base
+policy restrictive by default, so it never overrides regional filters:
+
+```yaml
+views:
+ - name: orders_view
+ cubes:
+ - join_path: orders
+ includes: "*"
+
+ access_policy:
+ - group: viewer
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["__NONE__"]
+
+ - group: viewer_north_america
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["US", "CA", "MX"]
+```
+
+The `viewer` policy now filters `country = '__NONE__'`, a value that doesn't
+exist in the data. On its own, this grants access to zero rows.
+
+When a user belongs to both groups, Cube combines the two row filters with OR:
+
+```sql
+WHERE (country = '__NONE__') OR (country IN ('US', 'CA', 'MX'))
+```
+
+The first condition matches nothing, so only the regional filter takes effect.
+The user sees exactly the rows allowed by their regional group.
+
+## Adding more regions
+
+Scale this pattern by adding a policy per regional group. The base `viewer`
+policy continues to act as the member-access gateway while granting no rows on
+its own:
+
+```yaml
+views:
+ - name: orders_view
+ cubes:
+ - join_path: orders
+ includes: "*"
+
+ access_policy:
+ - group: viewer
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["__NONE__"]
+
+ - group: viewer_north_america
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["US", "CA", "MX"]
+
+ - group: viewer_europe
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["DE", "FR", "GB"]
+
+ - group: viewer_asia
+ member_level:
+ includes: "*"
+ row_level:
+ filters:
+ - member: country
+ operator: equals
+ values: ["JP", "KR", "SG"]
+```
+
+A user in `viewer` + `viewer_north_america` + `viewer_europe` sees rows where
+`country` is in `US, CA, MX, DE, FR, GB`. The dummy `__NONE__` filter from the
+base group adds nothing to the result.
+
+## Result
+
+For a user in both `viewer` and `viewer_north_america`, Cube generates SQL like:
+
+```sql
+SELECT
+ count(*) "orders_view__count"
+FROM orders
+WHERE
+ (country = '__NONE__')
+ OR (country IN ('US', 'CA', 'MX'))
+LIMIT 10000
+```
+
+Only North American rows are returned. The base group grants member access
+without widening the row-level filter.
diff --git a/docs-mintlify/recipes/index.mdx b/docs-mintlify/recipes/index.mdx
index 363f21c3bbee4..c7d12dcaa2d45 100644
--- a/docs-mintlify/recipes/index.mdx
+++ b/docs-mintlify/recipes/index.mdx
@@ -4,7 +4,7 @@ description: Step-by-step tutorials and best practices for getting the most out
mode: wide
---
-Explore **38 recipes** across data modeling, calculations, analytics patterns,
+Explore **39 recipes** across data modeling, calculations, analytics patterns,
pre-aggregations, configuration, APIs, and AI.
## Data Modeling
@@ -28,6 +28,9 @@ pre-aggregations, configuration, APIs, and AI.
Combine multiple database tables that relate to the same entity into a single cube.
+
+ Combine a base group for member access with regional groups that add row-level filters.
+
## Calculations & Metrics