diff --git a/docs/docs/how-tos/configure-keycloak-howto.md b/docs/docs/how-tos/configure-keycloak-howto.md
index f949894fa..47e93947c 100644
--- a/docs/docs/how-tos/configure-keycloak-howto.md
+++ b/docs/docs/how-tos/configure-keycloak-howto.md
@@ -132,57 +132,6 @@ Your new user can now log in to Nebari, visit your provided Nebari domain URI wh

-## In-depth look at Roles and Groups
-
-Groups represent a collection of users that perform similar actions and therefore require similar permissions. By default, Nebari is deployed with the following groups: `admin`, `developer`, and `analyst` (in roughly descending order of permissions and scope).
-
-:::info
-Users in a particular group will also get access to that groups shared folder. So if `user A` belongs to the `developer`, they will also have access to the `~/shared/developer` folder. This also applies to new groups that you create.
-:::
-
-Roles on the other hand represent the type or category of user. This includes access and permissions that this category of user will need to perform their regular job duties. The differences between `groups` and `roles` are subtle. Particular roles (one or many), like `conda_store_admin`, are associated with a particular group, such as `admin` and any user in this group will then assume the role of `conda_store_admin`.
-
-:::info
-These roles can be stacked. This means that if a user is in one group with role `conda_store_admin` and another group with role `conda_store_viewer`, this user ultimately has the role `conda_store_admin`.
-:::
-
-| Group | Access to Nebari Resources | Roles | Permissions Description |
-| ------------ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `analyst` |
- Conda-Store
- Jupyterhub
- Argo Workflows
- Grafana
| - `conda_store_developer`
- `jupyterhub_developer`
- `argo_viewer`
- `grafana_viewer`
| - Default user permissions
- Access to start a server instance and generate JupyterHub access token.
- Read/write access to shared `analyst` folder group mount
- Read access to `analyst` and write access to personal conda-store namespace
- Read access to Argo-Workflows and Jupyter-Scheduler
- Inherent Grafana permissions from [Grafana viewer scopes](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/#organization-roles)
|
-| `developer` | - Conda-Store
- Dask
- Jupyterhub
- Argo Workflows
- Grafana Developer
| - `conda_store_developer`
- `dask_developer`
- `jupyterhub_developer`
- `argo_developer`
- `grafana_developer`
| - All of the above access, plus...
- Read access `developer` conda-store namespace
- Access to create Dask clusters.
- Read/write access to shared `developer` folder group mount
- Read/create access to Argo-Workflows and Jupyter-Scheduler
- Inherent Grafana permissions from [Grafana editor scopes](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/#organization-roles)
|
-| `admin` | - Conda-Store
- Dask
- Jupyterhub
- Argo Workflows
- Grafana
| - `conda_store_admin`
- `dask_admin`
- `jupyterhub_admin`
- `argo_admin`
- `grafana_admin`
| - All of the above access, plus...
- Read/write access to all conda-store available namespaces/environments.
- Access to Jupyterhub Admin page and can access JupyterLab users spaces
- Access to Keycloak and can add remove users and groups
- Inherent Grafana permissions from [Grafana administrator scopes](https://grafana.com/docs/grafana/latest/administration/roles-and-permissions/#organization-roles)
|
-| `superadmin` | - Conda-Store
- Dask
- Jupyterhub
- Argo Workflows
- Grafana
| - `conda_store_superadmin`
- `dask_admin`
- `jupyterhub_admin`
- `argo_admin`
- `realm_admin` (Keycloak)
- `grafana_admin`
| - All of the above access, plus...
- Delete (build and environment) access on conda-store
- Full access to Keycloak (realm) (same as `root`)
|
-
-:::info
-Check [Conda-store authorization model](https://conda-store.readthedocs.io/en/latest/contributing.html#authorization-model) for more details on conda-store authorization.
-:::
-
-:::caution
-The role `jupyterhub_admin` gives users elevated permissions to JupyterHub and should be applied judiciously. As mentioned in the table above, a JupyterHub admin is able to impersonate other users and view the contents of their home folder. For more details, read through the [JupyterHub documentation](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-management.html#admin-users).
-:::
-
-To create new groups or modify (or delete) existing groups, log in as `root` and click **Groups** on the left-hand side.
-
-As an example, we create a new group named `conda-store-manager`. This group will have administrator access to the [Conda-Store service].
-
-1. Click **New** in the upper-right hand corner under **Groups**.
-
-
-
-- Then, give the new group an appropriate name.
-
-
-
-2. Under **Role Mapping**, add the appropriate **Client Roles** as needed; there should be no need to update the **Realm Roles**.
-
-
-
-In this example, the new group only has one mapped role, `conda_store_admin`; however, it's possible to attach multiple **Client Roles** to a single group.
-
-
-
-Once complete, return to the **Users** section in the dashboard and add the relevant users to this newly created group.
-
[keycloak-login]: /docs/tutorials/login-keycloak
diff --git a/docs/docs/how-tos/fine-grained-permissions.md b/docs/docs/how-tos/fine-grained-permissions.md
deleted file mode 100644
index f682acb22..000000000
--- a/docs/docs/how-tos/fine-grained-permissions.md
+++ /dev/null
@@ -1,125 +0,0 @@
-# Fine Grained Permissions via Keycloak
-
-Nebari provides its users (particularly admins) a way to manage roles and permissions to
-various services like `jupyterhub` and `conda-store` via Keycloak. The idea is to be able to manage
-roles and permissions from a central place, in this case Keycloak. An admin or anyone who has
-permissions to create a role in Keycloak will create role(s) with assigned scopes (permissions)
-to it and attach it to user(s) or group(s).
-
-These roles are created and attached from keycloak's interface and scoped for a particular
-client (i.e. a Nebari service such as `jupyterhub` or `conda-store`). This means the roles for a
-particular service (say `jupyterhub`) should be created within the Keycloak client named
-`jupyterhub`.
-
-By default, Nebari comes with several custom clients included in a fresh deployment.
-These clients facilitate various services and integrations within the Nebari ecosystem.
-The predefined clients are as follows:
-
-```yaml
-clients:
- - jupyterhub
- - conda_store
- - grafana (if monitoring is enabled)
- - argo-server-sso (if argo is enabled)
- - forwardauth
-```
-
-To manage and configure these clients, you can navigate to the `Clients` tab within the
-Keycloak admin console, as illustrated in the image below.
-
-
-
-This can be accessed at `/auth/admin/master/console/#/realms/nebari/clients`
-
-## Creating a Role
-
-The process for creating a role is similar, irrespective of the service. To create a role for a
-service
-
-1. Select the appropriate client and click on "Add Role".
-
-
-
-2. On the "Add Role" form, write a meaningful name and description for the role. Be sure to include what this role intends to accomplish. Click "Save".
-
-
-
-3. Now the role has been created, but it does nothing. Let's add some permissions to it by clicking on the "Attributes" tab
- and adding scopes. The following sections will explain the `components` and `scopes` in more detail.
-
- 
-
-## Adding Role to Group(s) / User(s)
-
-Creating a role in Keycloak has no effect on any user or group's permissions. To grant a set of permissions
-to a user or group, we need to _attach_ the role to the user or group. To add a role to a user:
-
-1. Select users on the left sidebar and enter the username in the Lookup searchbar.
-
- 
-
-2. Select that user and click on the "Role Mappings" tab.
-
-
-
-3. Select the Client associated with the Role being added.
-
-
-
-4. Select the role in the "Available Roles" and click on "Add Selected >>".
-
-
-
-To attach a role to a group, follow the above steps by clicking on the groups tab and
-selecting a group instead of selecting the user in the first step.
-
-In the above section, we learned how to create a role with some attributes and attach it to a user or a group.
-Now we will learn how to create scopes to grant a particular set of permissions to the user.
-
-:::note
-After the roles are assigned to a user or group in Keycloak, the user **must** logout and login back in to the service
-for the roles to take in effect. For example let's say we add a set of roles for `conda-store` to the user named
-"John Doe", now for the user "John Doe" to be able to avail newly granted/revoked roles, they need to login to
-conda-store again (similarly for `jupyterhub` as well), after the roles are granted/revoked.
-:::
-
-### Components Attribute
-
-We have seen in the above example the `component` attribute while creating a role. The value of this parameter
-depends on the type of component in the service, we're creating a role for, currently we only have two components:
-
-- `jupyterhub`: to create `jupyterhub` native roles in the `jupyterhub` client.
-- `conda-store`: to create `conda-store` roles in the `conda_store` client
-
-### JupyterHub Scopes
-
-The syntax for the `scopes` attribute for a `jupyterhub` role in Keycloak in Nebari follows the native RBAC scopes syntax
-for JupyterHub itself. The documentation can be found [here](https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#scope-conventions).
-
-As an example, scopes for allowing users to share apps in Nebari's `jhub-apps` launcher may look like this:
-
-> `shares!user,read:users:name,read:groups:name`
-
-The `scopes` defined above consists of three scopes:
-
-- `shares!user`: grants permissions to share user's server
-- `read:users:name`: grants permissions to read other user's names
-- `read:groups:name`: grants permissions to read other groups's names
-
-To be able to share a server to a group or a user you need to be able to read other user's or group's names and must have
-permissions to be able to share your server, this is what this set of permissions implement.
-
-### Conda Store Scopes
-
-The scopes for roles for the `conda-store` Client are applied to the `namespace` level of `conda-store`.
-
-Below is example of granting a user specialized permissions to `conda-store`:
-
-> `admin!namespace=analyst,developer!namespace=nebari-git`
-
-The `scopes` defined above consists of two scopes:
-
-- `admin!namespace=analyst`: grants `admin` access to namespace `analyst`
-- `developer!namespace=nebari-git`: grants `developer` access to namespace `nebari-git`
-
-When attached to a user or a group, the above-mentioned permissions will be granted to the user/group.
diff --git a/docs/docs/how-tos/fine-grained-permissions.mdx b/docs/docs/how-tos/fine-grained-permissions.mdx
new file mode 100644
index 000000000..eff682f4f
--- /dev/null
+++ b/docs/docs/how-tos/fine-grained-permissions.mdx
@@ -0,0 +1,363 @@
+---
+title: Fine-Grained Permissions via Keycloak
+description: Learn how to manage roles, groups, and scopes in Keycloak to grant fine-grained permissions in Nebari.
+---
+Nebari uses [Keycloak](https://www.keycloak.org/) to centrally manage roles,
+permissions, and scopes for its core services like `jupyterhub` and `conda-store`. By
+default, Nebari provides:
+
+- **Predefined roles** (e.g., `conda_store_admin`, `jupyterhub_developer`)
+- **Default groups** (`admin`, `developer`, `analyst`, `superadmin`)
+- **Clients** corresponding to key Nebari services
+
+These defaults cover most common access requirements. However, when more specific or
+granular access control is needed, administrators can create custom roles and scopes in
+Keycloak, see [Creating Custom Roles with Scopes](#creating-custom-roles-with-scopes).
+
+## Default Clients and Groups in Nebari
+
+A fresh Nebari deployment comes with several custom Keycloak clients (services) enabled.
+You can manage these from **Clients** in the Keycloak admin console:
+
+```yaml
+clients:
+ - jupyterhub
+ - conda_store
+ - grafana (if monitoring is enabled)
+ - argo-server-sso (if argo is enabled)
+ - forwardauth
+```
+
+Users are organized into default groups: `admin`, `developer`, `analyst`, and
+`superadmin`. Each group has one or more default roles that define their access level
+across Nebari services.
+
+:::info Shared Folders
+Membership in one of the default groups also grants access to the corresponding shared
+folder (e.g., the `developer` group has access to `~/shared/developer`). Since,
+`2024.9.1` Nebari now required a special role to access the shared folders
+`Allow-group-directory-creation-role`.
+:::
+
+## In-depth Look at Default Roles and Groups
+
+Groups represent a collection of users that perform similar actions and therefore
+require similar permissions. By default, Nebari is deployed with the following groups:
+`admin`, `developer`, and `analyst` (in roughly descending order of permissions and
+scope).
+
+Roles on the other hand represent the type or category of user. This includes access and
+permissions that this category of user will need to perform their regular job duties.
+The differences between `groups` and `roles` are subtle. Particular roles (one or many),
+like `conda_store_admin`, are associated with a particular group, such as `admin` and
+any user in this group will then assume the role of `conda_store_admin`.
+
+:::tip Technical Note
+Roles in nebari can stack, meaning that if a user is in one group with role
+`conda_store_admin` and another group with role `conda_store_viewer`, this user
+ultimately has the role `conda_store_admin`. Also, in case of different level of access
+originating from different groups, precedence is given to the highest level of access.
+:::
+
+Below is a table that outlines the default roles, groups, and permissions that come with
+Nebari:
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+import { Table } from '@site/src/components/MarkdownTable';
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+:::note
+See [Conda-Store Authorization
+Model](https://conda-store.readthedocs.io/en/latest/contributing.html#authorization-model)
+for additional details.
+:::
+
+:::warning
+The `jupyterhub_admin` role grants permission to impersonate other users, including
+viewing their home folder contents. Use it sparingly and refer to the [JupyterHub
+docs](https://z2jh.jupyter.org/en/stable/jupyterhub/customizing/user-management.html#admin-users)
+for more info.
+:::
+
+## Roles, Groups, and Scopes
+
+When managing user access in Nebari, it's helpful to understand how **roles**,
+**groups**, and **scopes** work together:
+
+- **Roles**: A named set of permissions, while also providing a way to categorize users
+ by their access level or job function. Example: `conda_store_developer` or
+ `jupyterhub_admin`.
+- **Groups**: Collections of users who need similar permissions. For example, the
+ `developer` group might contain multiple roles like `conda_store_developer` and
+ `jupyterhub_developer`.
+- **Scopes**: A Service's fine-grained permission entity within a role. For instance, a
+ JupyterHub scope might be `read:users:name` (to read other users’ names), while a
+ Conda-Store scope might be `admin!namespace=analyst` (to grant admin-level access in
+ the `analyst` namespace).
+
+Whenever you create or modify a role in Keycloak, it won’t affect anyone until it’s
+assigned to a group or user. Below is the general process:
+
+**Typical Workflow**:
+1. **Create or edit** a role (and define its scopes) under the relevant **Client**
+ (e.g., `jupyterhub`, `conda_store`) in Keycloak.
+2. **Assign** this role to a **group** (or directly to an individual user).
+3. **Add users** to that group if you haven’t already.
+
+:::note
+Users must log out and log back in for newly assigned roles to take effect.
+:::
+
+### Granting Permissions to a User
+
+Let’s say you have someone who only needs *administrator-level privileges for
+Conda-Store*, while leaving other services at their default access.
+
+As an example, we create a new group named `conda-store-manager`. This group will have
+administrator access to the `Conda-Store` service.
+
+1. Click **New** in the upper-right hand corner under **Groups**.
+
+
+
+- Then, give the new group an appropriate name.
+
+
+
+2. Under **Role Mapping**, add the appropriate **Client Roles** as needed; there should
+ be no need to update the **Realm Roles**.
+
+
+
+In this example, the new group only has one mapped role, `conda_store_admin`; however,
+it's possible to attach multiple **Client Roles** to a single group.
+
+
+
+Once complete, return to the **Users** section in the dashboard and add the relevant
+users to this newly created group.
+
+:::note
+To create new groups or modify (or delete) existing groups, log in as `root` and click
+**Groups** on the left-hand side.
+:::
+
+### Creating Custom Roles with Scopes
+
+Beyond Nebari’s default roles, you can also create your own with highly specific
+permissions. The process for creating a role is similar, irrespective of the service.
+
+:::warning
+Nebari currently only supports custom roles for the `jupyterhub` and `conda_store`.
+Future releases may extend this to other services.
+:::
+
+#### Components and Syntax
+
+In Nebari, **scopes** are closely tied to the service (client) you’re working
+with— In Keycloak's roles we identify them as “components.”
+
+- **Components**:
+ Specifies the service that a role’s scopes apply to (e.g., `jupyterhub` or `conda-store`).
+- **Scopes**:
+ The actual permission strings recognized by that service.
+
+
+
+#### JupyterHub Scopes
+
+JupyterHub scopes syntax in Nebari follow [JupyterHub’s built-in RBAC
+syntax](https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html). For example:
+```
+shares!user,read:users:name,read:groups:name
+```
+- `shares!user`: Allows sharing servers with other users.
+- `read:users:name`: Grants read access to other users’ names.
+- `read:groups:name`: Grants read access to other groups’ names.
+
+This combination allows a user to share their server with others (via `shares!user`) and
+also read other users’ and groups’ names.
+
+For a complete list of JupyterHub scopes, see the
+[https://jupyterhub.readthedocs.io/en/stable/rbac/scopes.html#available-scopes](JupyterHub
+documentation).
+
+Nebari extend these scoped including a new *share* scope that is reserved for
+`Jhub-apps` usage, allowing, as seen above for sharing servers with other users,
+including more specifcaly jhub-apps applications (apps).
+
+---
+
+#### Conda-Store Scopes
+
+Conda-Store scopes are defined at the namespace level. For example:
+```
+admin!namespace=analyst,developer!namespace=nebari-git
+```
+- `admin!namespace=analyst`: Grants an **admin** role in the `analyst` namespace.
+- `developer!namespace=nebari-git`: Grants a **developer** role in the `nebari-git` namespace.
+
+This grants an **admin** role in the `analyst` namespace, plus a **developer** role in
+the `nebari-git` namespace. When assigned to a user or group, these permissions apply
+only to those specified namespaces in Conda-Store.
+
+Conda-store scopes follow a much simpler syntax than JupyterHub scopes, as they are only
+defined at the namespace level:
+```
+
!namespace=
+```
+whereas the `access_level` there entices the available conda-store' roles as per its own
+documentation
+[Conda-Store
+role-mappings](https://conda.store/conda-store/explanations/conda-store-concepts#role-mappings).
+
+
+
+### Creating a Role
+
+In the following example, we create a new role for the `JupyterHub` client granting the
+same level of permission outlined in the section above [JupyterHub Scopes](#jupyterhub-scopes).
+
+1. Select the appropriate client and click on "Add Role".
+
+
+1. On the "Add Role" form, write a meaningful name and description for the role. Be sure
+ to include what this role intends to accomplish. Click "Save".
+
+
+
+3. Now the role has been created, but it does nothing. Let's add some permissions to it
+ by clicking on the "Attributes" tab and adding scopes. The following sections will
+ explain the `components` and `scopes` in more detail.
+
+ 
+
+To make these new permissions effective, assign the custom role to a user or group (via
+**Role Mappings**). Again, any user changes require a re-login to become active.
diff --git a/docs/package.json b/docs/package.json
index f287dba8a..f2e94bd52 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -44,11 +44,13 @@
"@docusaurus/preset-classic": "3.5.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.1.1",
+ "dedent": "^0.7.0",
"docusaurus-lunr-search": "^3.3.0",
"docusaurus-plugin-sass": "^0.2.5",
"prism-react-renderer": "^2.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-markdown": "^8.0.7",
"sass": "^1.77.8"
},
"devDependencies": {
diff --git a/docs/src/components/MarkdownTable/index.tsx b/docs/src/components/MarkdownTable/index.tsx
new file mode 100644
index 000000000..eb882aff8
--- /dev/null
+++ b/docs/src/components/MarkdownTable/index.tsx
@@ -0,0 +1,46 @@
+import React from 'react';
+import ReactMarkdown from 'react-markdown';
+import dedent from 'dedent';
+
+/**
+ * Each row in the table is an array of cells.
+ * Each cell can be either:
+ * - A single Markdown string, OR
+ * - An array of Markdown lines (which we'll join with '\n').
+ */
+export interface MarkdownTableProps {
+ headers: string[];
+ rows: Array>;
+}
+
+export function Table({ headers, rows }: MarkdownTableProps) {
+ return (
+
+
+
+ {headers.map((header, headerIdx) => (
+ {header} |
+ ))}
+
+
+
+ {rows.map((rowData, rowIdx) => (
+
+ {rowData.map((cellData, cellIdx) => {
+ // If the cellData is an array of strings, join them with "\n"
+ const cellContent = Array.isArray(cellData)
+ ? dedent(cellData.join('\n'))
+ : dedent(cellData);
+
+ return (
+
+ {cellContent}
+ |
+ );
+ })}
+
+ ))}
+
+
+ );
+}