diff --git a/charts/grafana/templates/grafana-configmap.yaml b/charts/grafana/templates/grafana-configmap.yaml
index a681f9f..f1c6b84 100644
--- a/charts/grafana/templates/grafana-configmap.yaml
+++ b/charts/grafana/templates/grafana-configmap.yaml
@@ -63,7 +63,8 @@ data:
     [users]
     allow_sign_up = false
     allow_org_create = false
-    auto_assign_org = false
+    auto_assign_org = true
+    auto_assign_org_role = None
     login_hint = email or username
 
     [auth]
@@ -102,12 +103,12 @@ data:
 
     [quota]
     enabled = true
-    org_user = 2
+    org_user = -1
     org_dashboard = 20
     org_data_source = 5
     org_api_key = 2
     org_alert_rule = 20
-    user_org = 2
+    user_org = -1
     alerting_rule_group_rules = 20
     alerting_rule_evaluation_results = 20
 
diff --git a/rootfs/Dockerfile b/rootfs/Dockerfile
index 2515407..1824c4d 100644
--- a/rootfs/Dockerfile
+++ b/rootfs/Dockerfile
@@ -4,9 +4,9 @@ FROM registry.drycc.cc/drycc/base:${CODENAME}
 ARG DRYCC_UID=1001 \
   DRYCC_GID=1001 \
   DRYCC_HOME_DIR=/usr/share/grafana \
-  GRAFANA_VERSION="12.2.1" \
+  GRAFANA_VERSION="12.4.2" \
   JQ_VERSION="1.7.1" \
-  CADDY_VERSION="2.10.2" \
+  CADDY_VERSION="2.11.2" \
   PYTHON_VERSION="3.14"
 
 RUN groupadd drycc --gid ${DRYCC_GID} \
diff --git a/rootfs/usr/share/grafana/oauth2/datasources/prometheus.json b/rootfs/usr/share/grafana/oauth2/datasources/prometheus.json
index 4eefc33..aed8d47 100644
--- a/rootfs/usr/share/grafana/oauth2/datasources/prometheus.json
+++ b/rootfs/usr/share/grafana/oauth2/datasources/prometheus.json
@@ -2,7 +2,7 @@
     "name": "Prometheus on Drycc",
     "type": "prometheus",
     "uid": "prometheus_on_drycc",
-    "url": "${controller_url}/v2/prometheus/${username}",
+    "url": "${controller_url}/v2/prometheus/${workspace}",
     "access": "proxy",
     "isDefault": true,
     "basicAuth": false,
diff --git a/rootfs/usr/share/grafana/oauth2/datasources/quickwit.json b/rootfs/usr/share/grafana/oauth2/datasources/quickwit.json
index 8c1861b..1d1f012 100644
--- a/rootfs/usr/share/grafana/oauth2/datasources/quickwit.json
+++ b/rootfs/usr/share/grafana/oauth2/datasources/quickwit.json
@@ -2,7 +2,7 @@
     "name": "Application Logs",
     "type": "quickwit-quickwit-datasource",
     "uid": "application_logs",
-    "url": "${controller_url}/v2/quickwit/${username}",
+    "url": "${controller_url}/v2/quickwit/${workspace}",
     "access": "proxy",
     "basicAuth": false,
     "jsonData": {
diff --git a/rootfs/usr/share/grafana/oauth2/hook/grafana.py b/rootfs/usr/share/grafana/oauth2/hook/grafana.py
index fc2a2fd..3721bd8 100644
--- a/rootfs/usr/share/grafana/oauth2/hook/grafana.py
+++ b/rootfs/usr/share/grafana/oauth2/hook/grafana.py
@@ -1,3 +1,4 @@
+import logging
 import os
 import time
 import json
@@ -5,42 +6,31 @@
 from string import Template
 from psycopg import AsyncConnection
 
+logger = logging.getLogger(__name__)
+
 DEFAULT_HEADERS = {"Content-Type": "application/json"}
 DRYCC_CONTROLLER_URL = os.environ.get('DRYCC_CONTROLLER_URL')
 DRYCC_GRAFANA_REFRESH = os.environ.get('DRYCC_GRAFANA_REFRESH', '60s')
 DRYCC_GRAFANA_DASHBOARD = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../")
 
+# Drycc Workspace role to Grafana role mapping
+DRYCC_WORKSPACE_ROLE_MAPPING = {"admin": "Admin", "member": "Editor", "viewer": "Viewer"}
+# Grafana default main organization ID, renamed to "drycc" by init_org on startup
+DRYCC_ORG_ID = 1
 
-def api_url(url_path, is_admin=False):
-    if is_admin:
-        return "http://{}:{}@localhost:{}{}".format(
-            os.environ.get('GF_SECURITY_ADMIN_USER'),
-            os.environ.get('GF_SECURITY_ADMIN_PASSWORD'),
-            os.environ.get('GF_SERVER_HTTP_PORT', 3000),
-            url_path,
-        )
-    return "http://localhost:{}{}".format(os.environ.get('GF_SERVER_HTTP_PORT', 3000), url_path)
 
-
-def api_headers(context: dict, userinfo):
-    headers = {"Content-Type": "application/json"}
-    headers["Remote-User"] = userinfo["preferred_username"]
-    headers["Remote-Name"] = userinfo["preferred_username"]
-    headers["Remote-Email"] = userinfo["email"]
-    if "org_id" in context:
-        headers["X-Grafana-Org-Id"] = str(context["org_id"])
-    return headers
+# ── Public functions (exported via __init__.py) ──────────────────────────
 
 
-async def init_org(org_id=1, name="drycc"):
+async def init_org(org_id=DRYCC_ORG_ID, name="drycc"):
     headers = {"Content-Type": "application/json"}
     async with httpx.AsyncClient() as client:
-        resp = await client.get(api_url(f"/api/orgs/{org_id}", is_admin=True), headers=headers)
+        resp = await client.get(_api_url(f"/api/orgs/{org_id}", is_admin=True), headers=headers)
         org = resp.json()
         if org["name"] == name:
             return
         await client.put(
-            api_url(f"/api/orgs/{org_id}", is_admin=True),
+            _api_url(f"/api/orgs/{org_id}", is_admin=True),
             headers=headers,
             json={"name": name},
         )
@@ -48,145 +38,397 @@ async def init_org(org_id=1, name="drycc"):
 
 async def sync_user(context: dict, token: dict, userinfo: dict):
     async with httpx.AsyncClient() as client:
-        resp = await client.get(api_url("/api/user"), headers=api_headers(context, userinfo))
+        resp = await client.get(_api_url("/api/user"), headers=_api_headers(context, userinfo))
         user = resp.json()
         context["user_id"] = user["id"]
         if userinfo["is_superuser"]:
             await client.put(
-                api_url(f"/api/admin/users/{context["user_id"]}/permissions", is_admin=True),
-                headers=api_headers(context, userinfo),
+                _api_url(f"/api/admin/users/{context['user_id']}/permissions", is_admin=True),
+                headers=_api_headers(context, userinfo),
                 json={"isGrafanaAdmin": userinfo["is_superuser"]},
             )
 
 
 async def sync_role(context: dict, token: dict, userinfo: dict):
+    """Sync user's Grafana Org memberships based on their workspace memberships."""
+    created, drycc_token = await _get_or_create_drycc_token(
+        userinfo["preferred_username"], token)
+    context["drycc_token"] = drycc_token
+
+    workspace_orgs = await _build_workspace_orgs(userinfo, drycc_token)
+
     async with httpx.AsyncClient() as client:
-        resp = await client.get(api_url("/api/user/orgs"), headers=api_headers(context, userinfo))
-        orgs = resp.json()
-        has_admin_org = False
-        for org in orgs:
-            if org["orgId"] == 1:
-                has_admin_org = True
-            else:
-                context["org_id"] = org["orgId"]
-        if has_admin_org and not (userinfo["is_superuser"] or userinfo["is_staff"]):
-            await client.delete(
-                api_url(f"/api/orgs/1/users/{context["user_id"]}", is_admin=True),
-                headers=api_headers(context, userinfo),
-            )
-        elif not has_admin_org and (userinfo["is_superuser"] or userinfo["is_staff"]):
-            await client.post(
-                api_url("/api/orgs/1/users", is_admin=True),
-                headers=api_headers(context, userinfo),
-                json={"loginOrEmail": userinfo["preferred_username"], "role": "Admin"},
-            )
-    await init_org(org_id=context["org_id"], name=userinfo["preferred_username"])
+        resp = await client.get(
+            _api_url("/api/user/orgs"), headers=_api_headers(context, userinfo))
+        current_orgs = resp.json()
+        workspace_org_ids = {info["org_id"] for info in workspace_orgs.values()}
+        current_org_map = {org["orgId"]: org["name"] for org in current_orgs}
+
+        await _sync_workspace_org_memberships(
+            client, context, userinfo, workspace_orgs, current_org_map)
+        await _cleanup_stale_orgs(
+            client, context, userinfo, current_orgs, workspace_org_ids)
+        await _sync_drycc_org(client, context, userinfo, current_org_map)
+
+    context["workspace_orgs"] = workspace_orgs
 
 
 async def sync_default(context: dict, token: dict, userinfo: dict):
-    alertmanager_default_config = json.dumps({
-        "alertmanager_config": {
-            "route": {
-                "receiver": "grafana-default-email",
-                "group_by": ["grafana_folder", "alertname"]
-            },
-            "receivers": [{
-                "name": "grafana-default-email",
-                "grafana_managed_receiver_configs": [{
-                    "uid": "",
-                    "name": "email receiver",
-                    "type": "email",
-                    "settings": {
-                        "addresses": f"{userinfo["email"]}"
-                    }
-                }]
-            }]
-        }
-    })
-    async with httpx.AsyncClient() as client:
-        await client.post(
-            api_url("/api/folders"),
-            headers=api_headers(context, userinfo),
-            json={"uid": "drycc", "title": "drycc"},
-        )
-    sql_tpl = f"""
-    INSERT INTO alert_configuration (
-        alertmanager_configuration, configuration_version, created_at, "default", org_id
-    )
-    VALUES (
-        '{alertmanager_default_config}', 'v1', {int(time.time())}, TRUE, {context["org_id"]}
-    )
-    ON CONFLICT (org_id) DO NOTHING;
+    """Create default folder and alert configuration for each workspace org.
+
+    The email receiver addresses are fully rebuilt each time based on
+    all workspace members with alerts=True, ensuring stale entries are removed.
     """
-    async with await AsyncConnection.connect(os.environ.get("GF_DATABASE_URL")) as conn:
-        async with conn.cursor() as cursor:
-            await cursor.execute(sql_tpl)
-            await conn.commit()
+    workspace_orgs = context.get("workspace_orgs", {})
+    drycc_token = context.get("drycc_token")
+
+    for ws_name, ws_info in workspace_orgs.items():
+        org_id = ws_info["org_id"]
+        alerts = ws_info["alerts"]
+
+        alert_addresses = await _build_alert_addresses(ws_name, drycc_token, userinfo, alerts)
+        alertmanager_config = _build_alertmanager_config(alert_addresses)
+
+        ctx = {**context, "org_id": org_id}
+        async with httpx.AsyncClient() as client:
+            await client.post(
+                _api_url("/api/folders"),
+                headers=_api_headers(ctx, userinfo),
+                json={"uid": "drycc", "title": "drycc"},
+            )
+
+        await _upsert_alert_configuration(org_id, alertmanager_config)
 
 
 async def sync_alerting(context: dict, token: dict, userinfo: dict):
+    """Create or update alert rules for each workspace org (idempotent).
+
+    Alert rules are always created regardless of the alerts field.
+    The alerts field only controls notification channels (handled in sync_default).
+    """
+    workspace_orgs = context.get("workspace_orgs", {})
     alerting_path = os.path.join(os.path.dirname(__file__), "..", "alerting")
-    async with httpx.AsyncClient() as client:
-        for filename in os.listdir(alerting_path):
-            with open(os.path.join(alerting_path, filename)) as f:
-                await client.post(
-                    api_url("/api/v1/provisioning/alert-rules"),
-                    headers=api_headers(context, userinfo),
-                    json=json.load(f),
+
+    for ws_name, ws_info in workspace_orgs.items():
+        org_id = ws_info["org_id"]
+        ctx = {**context, "org_id": org_id}
+
+        async with httpx.AsyncClient() as client:
+            for filename in os.listdir(alerting_path):
+                with open(os.path.join(alerting_path, filename)) as f:
+                    rule = json.load(f)
+                # Use PUT for idempotent upsert (POST would create duplicates)
+                resp = await client.put(
+                    _api_url(f"/api/v1/provisioning/alert-rules/{rule['uid']}"),
+                    headers=_api_headers(ctx, userinfo),
+                    json=rule,
                 )
+                if resp.status_code == 404:
+                    # Rule doesn't exist yet, create it
+                    await client.post(
+                        _api_url("/api/v1/provisioning/alert-rules"),
+                        headers=_api_headers(ctx, userinfo),
+                        json=rule,
+                    )
 
 
 async def sync_datasources(context: dict, token: dict, userinfo: dict):
-    headers = api_headers(context, userinfo)
+    """Create datasources for each workspace org with workspace-specific URLs."""
+    workspace_orgs = context.get("workspace_orgs", {})
     datasources_path = os.path.join(os.path.dirname(__file__), "..", "datasources")
-    created, drycc_token = await _get_or_create_drycc_token(userinfo["preferred_username"], token)
-    async with httpx.AsyncClient() as client:
-        for filename in os.listdir(datasources_path):
-            with open(os.path.join(datasources_path, filename)) as f:
-                template = Template(f.read())
-                datasource = json.loads(template.substitute(
-                    controller_url=DRYCC_CONTROLLER_URL,
-                    username=userinfo["preferred_username"],
-                    time_interval=DRYCC_GRAFANA_REFRESH,
-                    token=drycc_token
-                ))
-                resp = await client.get(
-                    api_url(f"/api/datasources/name/{datasource["name"]}"), headers=headers)
-                if resp.status_code == 200:
-                    if created:
-                        datasource = resp.json()
-                        datasource["secureJsonData"] = {
+    drycc_token = context.get("drycc_token")
+
+    for ws_name, ws_info in workspace_orgs.items():
+        org_id = ws_info["org_id"]
+        ctx = {**context, "org_id": org_id}
+        headers = _api_headers(ctx, userinfo)
+
+        async with httpx.AsyncClient() as client:
+            for filename in os.listdir(datasources_path):
+                with open(os.path.join(datasources_path, filename)) as f:
+                    template = Template(f.read())
+                    datasource = json.loads(template.substitute(
+                        controller_url=DRYCC_CONTROLLER_URL,
+                        workspace=ws_name,
+                        time_interval=DRYCC_GRAFANA_REFRESH,
+                        token=drycc_token
+                    ))
+                    resp = await client.get(
+                        _api_url(f"/api/datasources/name/{datasource['name']}"), headers=headers)
+                    if resp.status_code == 200:
+                        existing = resp.json()
+                        existing["secureJsonData"] = {
                             "httpHeaderValue1": f"Token {drycc_token}",
                         }
-                        await _set_datasource_read_only(False, datasource["id"])
+                        await _set_datasource_read_only(False, existing["id"])
                         await client.put(
-                            api_url(f"/api/datasources/uid/{datasource["uid"]}"),
-                            headers=headers, json=datasource)
-                        await _set_datasource_read_only(True, datasource["id"])
-                elif resp.status_code == 404:
-                    resp = await client.post(
-                        api_url("/api/datasources"), headers=headers, json=datasource)
-                    await _set_datasource_read_only(True, resp.json()["datasource"]["id"])
-                else:
-                    raise ValueError(f"grafana returned an unexpected status: {resp.status_code}")
+                            _api_url(f"/api/datasources/uid/{datasource['uid']}"),
+                            headers=headers, json=existing)
+                        await _set_datasource_read_only(True, existing["id"])
+                    elif resp.status_code == 404:
+                        resp = await client.post(
+                            _api_url("/api/datasources"), headers=headers, json=datasource)
+                        await _set_datasource_read_only(True, resp.json()["datasource"]["id"])
+                    else:
+                        raise ValueError(
+                            f"grafana returned an unexpected status: {resp.status_code}"
+                        )
 
 
 async def sync_dashboards(context: dict, token: dict, userinfo: dict):
+    """Create dashboards for each workspace org."""
+    workspace_orgs = context.get("workspace_orgs", {})
     dashboards_path = os.path.join(os.path.dirname(__file__), "..", "dashboards")
+
+    for ws_name, ws_info in workspace_orgs.items():
+        org_id = ws_info["org_id"]
+        ctx = {**context, "org_id": org_id}
+
+        async with httpx.AsyncClient() as client:
+            for filename in os.listdir(dashboards_path):
+                with open(os.path.join(dashboards_path, filename)) as f:
+                    dashboard = json.load(f)
+                    dashboard.update({"id": None, "refresh": DRYCC_GRAFANA_REFRESH})
+                    await client.post(
+                        _api_url("/api/dashboards/db"),
+                        headers=_api_headers(ctx, userinfo),
+                        json={
+                            "dashboard": dashboard,
+                            "folderUid": "drycc",
+                            "overwrite": True,
+                        },
+                    )
+
+
+# ── Private functions ────────────────────────────────────────────────────
+
+
+def _api_url(url_path, is_admin=False):
+    if is_admin:
+        return "http://{}:{}@localhost:{}{}".format(
+            os.environ.get('GF_SECURITY_ADMIN_USER'),
+            os.environ.get('GF_SECURITY_ADMIN_PASSWORD'),
+            os.environ.get('GF_SERVER_HTTP_PORT', 3000),
+            url_path,
+        )
+    return "http://localhost:{}{}".format(os.environ.get('GF_SERVER_HTTP_PORT', 3000), url_path)
+
+
+def _api_headers(context: dict, userinfo):
+    headers = {"Content-Type": "application/json"}
+    headers["Remote-User"] = userinfo["preferred_username"]
+    headers["Remote-Name"] = userinfo["preferred_username"]
+    headers["Remote-Email"] = userinfo["email"]
+    if "org_id" in context:
+        headers["X-Grafana-Org-Id"] = str(context["org_id"])
+    return headers
+
+
+async def _get_workspaces(drycc_token: str) -> list:
+    """Call Controller API to get user's workspaces."""
+    headers = {"Authorization": f"Token {drycc_token}"}
     async with httpx.AsyncClient() as client:
-        for filename in os.listdir(dashboards_path):
-            with open(os.path.join(dashboards_path, filename)) as f:
-                dashboard = json.load(f)
-                dashboard.update({"id": None, "refresh": DRYCC_GRAFANA_REFRESH})
-                await client.post(
-                    api_url("/api/dashboards/db"),
-                    headers=api_headers(context, userinfo),
-                    json={
-                        "dashboard": dashboard,
-                        "folderUid": "drycc",
-                        "overwrite": True,
-                    },
-                )
+        resp = await client.get(
+            f"{DRYCC_CONTROLLER_URL}/v2/workspaces", headers=headers)
+        resp.raise_for_status()
+        return resp.json().get("results", [])
+
+
+async def _get_workspace_members(workspace_name: str, drycc_token: str) -> list:
+    """Call Controller API to get workspace members."""
+    headers = {"Authorization": f"Token {drycc_token}"}
+    async with httpx.AsyncClient() as client:
+        resp = await client.get(
+            f"{DRYCC_CONTROLLER_URL}/v2/workspaces/{workspace_name}/members",
+            headers=headers)
+        resp.raise_for_status()
+        return resp.json().get("results", [])
+
+
+async def _get_or_create_org(name: str) -> int:
+    """Find or create a Grafana Org by name. Returns org_id."""
+    headers = {"Content-Type": "application/json"}
+    async with httpx.AsyncClient() as client:
+        # Try to find existing org by name
+        resp = await client.get(
+            _api_url(f"/api/orgs/name/{name}", is_admin=True), headers=headers)
+        if resp.status_code == 200:
+            return resp.json()["id"]
+        # Create new org (handle race condition: another request may have created it)
+        resp = await client.post(
+            _api_url("/api/orgs", is_admin=True), headers=headers,
+            json={"name": name})
+        if resp.status_code == 409:
+            # Org already exists (race condition), fetch it
+            resp = await client.get(
+                _api_url(f"/api/orgs/name/{name}", is_admin=True), headers=headers)
+            return resp.json()["id"]
+        resp.raise_for_status()
+        return resp.json()["orgId"]
+
+
+async def _build_workspace_orgs(userinfo: dict, drycc_token: str) -> dict:
+    """Build workspace org info by fetching workspaces and their memberships.
+
+    Returns: {workspace_name: {"org_id": int, "role": str, "alerts": bool, "email": str}}
+    """
+    workspace_orgs = {}
+    try:
+        workspaces = await _get_workspaces(drycc_token)
+    except httpx.HTTPError as e:
+        logger.warning("Failed to fetch workspaces for %s: %s", userinfo["preferred_username"], e)
+        return workspace_orgs
+
+    for ws in workspaces:
+        workspace_name = ws["name"]
+        workspace_email = ws.get("email", userinfo["email"])
+        try:
+            members = await _get_workspace_members(workspace_name, drycc_token)
+        except httpx.HTTPError as e:
+            logger.warning("Failed to fetch members for workspace %s: %s", workspace_name, e)
+            continue
+        user_member = next(
+            (m for m in members if m["user"] == userinfo["preferred_username"]), None)
+        if user_member is None:
+            continue
+
+        org_id = await _get_or_create_org(workspace_name)
+        workspace_orgs[workspace_name] = {
+            "org_id": org_id,
+            "role": user_member["role"],
+            "alerts": user_member.get("alerts", True),
+            "email": workspace_email,
+        }
+    return workspace_orgs
+
+
+async def _sync_workspace_org_memberships(
+    client: httpx.AsyncClient, context: dict, userinfo: dict,
+    workspace_orgs: dict, current_org_map: dict,
+):
+    """Add/update user's membership in each workspace org."""
+    for ws_name, ws_info in workspace_orgs.items():
+        org_id = ws_info["org_id"]
+        grafana_role = DRYCC_WORKSPACE_ROLE_MAPPING.get(ws_info["role"], "Viewer")
+
+        if org_id in current_org_map:
+            await client.patch(
+                _api_url(f"/api/orgs/{org_id}/users/{context['user_id']}", is_admin=True),
+                headers=_api_headers(context, userinfo),
+                json={"role": grafana_role},
+            )
+        else:
+            await client.post(
+                _api_url(f"/api/orgs/{org_id}/users", is_admin=True),
+                headers=_api_headers(context, userinfo),
+                json={
+                    "loginOrEmail": userinfo["preferred_username"],
+                    "role": grafana_role,
+                },
+            )
+
+
+async def _cleanup_stale_orgs(
+    client: httpx.AsyncClient, context: dict, userinfo: dict,
+    current_orgs: list, workspace_org_ids: set,
+):
+    """Remove user from orgs they no longer belong to (excluding drycc org)."""
+    for org in current_orgs:
+        org_id = org["orgId"]
+        if org_id == DRYCC_ORG_ID:
+            continue
+        if org_id not in workspace_org_ids:
+            await client.delete(
+                _api_url(f"/api/orgs/{org_id}/users/{context['user_id']}", is_admin=True),
+                headers=_api_headers(context, userinfo),
+            )
+
+
+async def _sync_drycc_org(
+    client: httpx.AsyncClient, context: dict, userinfo: dict,
+    current_org_map: dict,
+):
+    """Handle drycc org membership for superusers/staff."""
+    has_drycc_org = DRYCC_ORG_ID in current_org_map
+    if has_drycc_org and not (userinfo["is_superuser"] or userinfo["is_staff"]):
+        await client.delete(
+            _api_url(f"/api/orgs/{DRYCC_ORG_ID}/users/{context['user_id']}", is_admin=True),
+            headers=_api_headers(context, userinfo),
+        )
+    elif not has_drycc_org and (userinfo["is_superuser"] or userinfo["is_staff"]):
+        await client.post(
+            _api_url(f"/api/orgs/{DRYCC_ORG_ID}/users", is_admin=True),
+            headers=_api_headers(context, userinfo),
+            json={"loginOrEmail": userinfo["preferred_username"], "role": "Admin"},
+        )
+    elif has_drycc_org and (userinfo["is_superuser"] or userinfo["is_staff"]):
+        await client.patch(
+            _api_url(f"/api/orgs/{DRYCC_ORG_ID}/users/{context['user_id']}", is_admin=True),
+            headers=_api_headers(context, userinfo),
+            json={"role": "Admin"},
+        )
+
+
+async def _build_alert_addresses(
+    ws_name: str, drycc_token: str, userinfo: dict, alerts: bool
+) -> str:
+    """Build comma-separated alert email addresses for a workspace."""
+    if drycc_token:
+        try:
+            members = await _get_workspace_members(ws_name, drycc_token)
+            email_list = [m["email"] for m in members if m.get("alerts", True)]
+            return ",".join(email_list)
+        except httpx.HTTPError as e:
+            logger.warning("Failed to fetch members for alert addresses in %s: %s", ws_name, e)
+            return userinfo["email"] if alerts else ""
+    return userinfo["email"] if alerts else ""
+
+
+def _build_alertmanager_config(alert_addresses: str) -> str:
+    """Build alertmanager JSON config string."""
+    if alert_addresses:
+        receivers = [{
+            "name": "grafana-default-email",
+            "grafana_managed_receiver_configs": [{
+                "uid": "",
+                "name": "email receiver",
+                "type": "email",
+                "settings": {"addresses": alert_addresses}
+            }]
+        }]
+    else:
+        receivers = [{
+            "name": "grafana-default-email",
+            "grafana_managed_receiver_configs": []
+        }]
+    return json.dumps({
+        "alertmanager_config": {
+            "route": {
+                "receiver": "grafana-default-email",
+                "group_by": ["grafana_folder", "alertname"]
+            },
+            "receivers": receivers
+        }
+    })
+
+
+async def _upsert_alert_configuration(org_id: int, config: str):
+    """Insert or update alert configuration for an org using parameterized query."""
+    async with await AsyncConnection.connect(os.environ.get("GF_DATABASE_URL")) as conn:
+        async with conn.cursor() as cursor:
+            await cursor.execute(
+                """
+                INSERT INTO alert_configuration (
+                    alertmanager_configuration, configuration_version, created_at, "default", org_id
+                ) VALUES (%s, %s, %s, %s, %s)
+                ON CONFLICT (org_id) DO UPDATE
+                SET alertmanager_configuration = EXCLUDED.alertmanager_configuration,
+                    configuration_version = EXCLUDED.configuration_version,
+                    created_at = EXCLUDED.created_at
+                """,
+                (config, "v1", int(time.time()), True, org_id),
+            )
+            await conn.commit()
 
 
 async def _set_datasource_read_only(read_only: bool, id: str):
@@ -210,7 +452,7 @@ async def _check_or_create_drycc_token(drycc_token, token):
                 if resp.status_code in [401, 403]:
                     created = True
             if created:
-                headers = {"Authorization": f"Bearer {token["access_token"]}"}
+                headers = {"Authorization": f"Bearer {token['access_token']}"}
                 data = (await client.post(
                     f"{DRYCC_CONTROLLER_URL}/v2/auth/token/?alias=grafana-datasource",
                     headers=headers, json=token)).json()
@@ -223,7 +465,8 @@ async def _check_or_create_drycc_token(drycc_token, token):
                 "SELECT o_auth_id_token FROM user_auth WHERE auth_module=%s AND auth_id=%s",
                 ("authproxy", username)
             )
-            drycc_token = (await cursor.fetchone())[0]
+            row = await cursor.fetchone()
+            drycc_token = row[0] if row else None
         created, drycc_token = await _check_or_create_drycc_token(drycc_token, token)
         if created:
             async with conn.cursor() as cursor:
