diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1b17f49748a407f3cbb7356a1875d37c65a82a82..30f7c157bab9534f4e1754e072de57338e18dbd2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -58,16 +58,15 @@ jobs:
   backend:
     if: always()
     runs-on: ubuntu-latest
-
-    env:
-      NODE_ENV: test
-
     steps:
     - uses: actions/checkout@v3
-    - name: Use Node.js 16.x
-      uses: actions/setup-node@v1
+    - uses: actions/setup-python@v4
       with:
-        node-version: 16.x
+        python-version: '3.10'
     - run: |
-        echo "busybody"
-  
+        cd backend
+        echo "hello world" >> index.html
+        export PATH_TO_PUBLIC=$(pwd)
+        pip install -r requirements.txt
+        pip install pytest
+        pytest
diff --git a/backend/app/sane_url.py b/backend/app/sane_url.py
index 947466933489b6509e6139df18d032864fbade43..29f14078e7aca259f2dc17c7815a74e9364c902e 100644
--- a/backend/app/sane_url.py
+++ b/backend/app/sane_url.py
@@ -4,7 +4,7 @@ from fastapi.exceptions import HTTPException
 from authlib.integrations.requests_client import OAuth2Session
 import requests
 import json
-from typing import Union, Dict, Optional
+from typing import Union, Dict, Optional, Any
 import time
 from io import StringIO
 from pydantic import BaseModel
@@ -27,6 +27,7 @@ vip_routes = [
 
 class SaneUrlDPStore(DataproxyStore):
     class AlreadyExists(Exception): ...
+    class NotWritable(IOError): ...
 
     @staticmethod
     def GetTimeMs() -> int:
@@ -36,8 +37,12 @@ class SaneUrlDPStore(DataproxyStore):
     def TransformKeyToObjName(key: str):
         return f"saneUrl/{key}.json"
     
+    writable = False
+
     def __init__(self, expiry_s=3 * 24 * 60 * 60):
-                
+        if not (SXPLR_EBRAINS_IAM_SA_CLIENT_ID and SXPLR_EBRAINS_IAM_SA_CLIENT_SECRET):
+            super().__init__(None, SXPLR_BUCKET_NAME)
+            return
         resp = requests.get(f"{EBRAINS_IAM_DISCOVERY_URL}/.well-known/openid-configuration")
         resp.raise_for_status()
         resp_json = resp.json()
@@ -54,6 +59,7 @@ class SaneUrlDPStore(DataproxyStore):
         self._refresh_token()
 
         super().__init__(self.token, SXPLR_BUCKET_NAME)
+        self.writable = True
     
     def _refresh_token(self):
         token_dict = self.session.fetch_token(self._token_endpoint, grant_type="client_credentials")
@@ -94,7 +100,8 @@ class SaneUrlDPStore(DataproxyStore):
             raise SaneUrlDPStore.GenericException(str(e)) from e
 
     def set(self, key: str, value: Union[str, Dict], request: Optional[Request]=None):
-        
+        if not self.writable:
+            raise SaneUrlDPStore.NotWritable
         object_name = SaneUrlDPStore.TransformKeyToObjName(key)
         try:
             super().get(object_name)
@@ -126,11 +133,19 @@ data_proxy_store = SaneUrlDPStore()
 @router.get("/{short_id:str}")
 async def get_short(short_id:str, request: Request):
     try:
-        existing_json = data_proxy_store.get(short_id)
+        existing_json: Dict[str, Any] = data_proxy_store.get(short_id)
         accept = request.headers.get("Accept", "")
         if "text/html" in accept:
             hashed_path = existing_json.get("hashPath")
-            return RedirectResponse(f"{HOST_PATHNAME}/#{hashed_path}")
+            extra_routes = []
+            for key in existing_json:
+                if key.startswith("x-"):
+                    extra_routes.append(f"{key}:{short_id}")
+                    continue
+
+            extra_routes_str = "" if len(extra_routes) == 0 else ("/" + "/".join(extra_routes))
+
+            return RedirectResponse(f"{HOST_PATHNAME}/#{hashed_path}{extra_routes_str}")
         return JSONResponse(existing_json)
     except DataproxyStore.NotFound as e:
         raise HTTPException(404, str(e))
diff --git a/backend/pytest.ini b/backend/pytest.ini
new file mode 100644
index 0000000000000000000000000000000000000000..bf8e4ff582ba9bad8361a33d53a8dcbd74aad89b
--- /dev/null
+++ b/backend/pytest.ini
@@ -0,0 +1,4 @@
+[pytest]
+pythonpath = .
+testpaths = 
+    test_app
diff --git a/backend/test_app/test_sane_url.py b/backend/test_app/test_sane_url.py
new file mode 100644
index 0000000000000000000000000000000000000000..49ab5ec495c797a1cff592c6f5dd1850034f3232
--- /dev/null
+++ b/backend/test_app/test_sane_url.py
@@ -0,0 +1,13 @@
+from app.app import app
+from fastapi.testclient import TestClient
+
+client = TestClient(app)
+
+def test_annotation_redirect():
+    resp = client.get("/go/stnr", headers={
+        "Accept": "text/html"
+    }, follow_redirects=False)
+    loc = resp.headers.get("Location")
+    assert loc, "Expected location header to be present, but was not"
+    assert "x-user-anntn:stnr" in loc, f"Expected the string 'x-user-anntn:stnr' in {loc!r}, but was not"
+
diff --git a/docs/releases/v2.13.2.md b/docs/releases/v2.13.2.md
new file mode 100644
index 0000000000000000000000000000000000000000..0f4731c3be36ebba0e4e174bab3e3539055f52b0
--- /dev/null
+++ b/docs/releases/v2.13.2.md
@@ -0,0 +1,11 @@
+# v2.13.2
+
+## Features
+
+- added visual display of errors relating to siibra-api
+
+## Bugfixes
+
+- fixed displaying annotation in `saneurl`
+- fixed feature fetching spinner
+- fixed feature fetching logic
diff --git a/e2e/checklist.md b/e2e/checklist.md
index 5e740efa83d8e722ee51853383ce0ece19bb25da..8378541a0f49d69c4ca72bd7a48d9489b2a185cb 100644
--- a/e2e/checklist.md
+++ b/e2e/checklist.md
@@ -72,10 +72,13 @@
 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/whs4) redirects to waxholm v4
 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/allen2017) redirects to allen 2017
 - [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/mebrains) redirects to monkey
+- [ ] [saneUrl](https://atlases.ebrains.eu/viewer-staging/saneUrl/stnr) redirects to URL that contains annotations
+
 ## VIP URL
 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/human) redirects to human mni152
 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/monkey) redirects mebrains
 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/rat) redirects to waxholm v4
 - [ ] [vipUrl](https://atlases.ebrains.eu/viewer-staging/mouse) redirects allen mouse 2017
+
 ## plugins
 - [ ] jugex plugin works
diff --git a/mkdocs.yml b/mkdocs.yml
index da0c7cd795225bb0141a01843c90337e00917ca3..af8c5c51fba201901758acb7b829ef8f572138f5 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -33,6 +33,7 @@ nav:
     - Fetching datasets: 'advanced/datasets.md'
     - Display non-atlas volumes: 'advanced/otherVolumes.md'
   - Release notes:
+    - v2.13.2: 'releases/v2.13.2.md'
     - v2.13.1: 'releases/v2.13.1.md'
     - v2.13.0: 'releases/v2.13.0.md'
     - v2.12.5: 'releases/v2.12.5.md'
diff --git a/package.json b/package.json
index 4a5fc8a314db8c6cb5f450c4b3fea5296a457b07..8ad925aa39bb3becb37abef12627dcb34ed9c5ff 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "siibra-explorer",
-  "version": "2.13.1",
+  "version": "2.13.2",
   "description": "siibra-explorer - explore brain atlases. Based on humanbrainproject/nehuba & google/neuroglancer. Built with angular",
   "scripts": {
     "lint": "eslint src --ext .ts",
diff --git a/src/atlasComponents/annotations/annotation.service.ts b/src/atlasComponents/annotations/annotation.service.ts
index d133671c3682ccf16c5af36271a12b90fbb4cf73..fd19915d2854db0302cff3f636316feacf075fc5 100644
--- a/src/atlasComponents/annotations/annotation.service.ts
+++ b/src/atlasComponents/annotations/annotation.service.ts
@@ -2,6 +2,7 @@ import { BehaviorSubject, Observable } from "rxjs";
 import { distinctUntilChanged } from "rxjs/operators";
 import { getUuid, waitFor } from "src/util/fn";
 import { PeriodicSvc } from "src/util/periodic.service";
+import { NehubaLayerControlService } from "src/viewerModule/nehuba/layerCtrl.service";
 
 export type TNgAnnotationEv = {
   pickedAnnotationId: string
@@ -54,6 +55,10 @@ interface NgAnnotationLayer {
     registerDisposer(fn: () => void): void
   }
   setVisible(flag: boolean): void
+  layerChanged: {
+    add(cb: () => void): void
+  }
+  visible: boolean
 }
 
 export class AnnotationLayer {
@@ -109,11 +114,13 @@ export class AnnotationLayer {
     this.nglayer.layer.registerDisposer(() => {
       this.dispose()
     })
+    NehubaLayerControlService.RegisterLayerName(this.name)
   }
   setVisible(flag: boolean){
     this.nglayer && this.nglayer.setVisible(flag)
   }
   dispose() {
+    NehubaLayerControlService.DeregisterLayerName(this.name)
     AnnotationLayer.Map.delete(this.name)
     this._onHover.complete()
     while(this.onDestroyCb.length > 0) this.onDestroyCb.pop()()
diff --git a/src/atlasComponents/sapi/sapi.service.spec.ts b/src/atlasComponents/sapi/sapi.service.spec.ts
index 7232406894823ca0f75994ac0fbb2536e26b08e4..ba9a8bfca5540076b45c795c9a4156ece347a79a 100644
--- a/src/atlasComponents/sapi/sapi.service.spec.ts
+++ b/src/atlasComponents/sapi/sapi.service.spec.ts
@@ -53,12 +53,11 @@ describe("> sapi.service.ts", () => {
           })
         })
         it("> should call fetch twice", async () => {
-          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          expect(fetchSpy).toHaveBeenCalledTimes(1)
           
           const allArgs = fetchSpy.calls.allArgs()
-          expect(allArgs.length).toEqual(2)
+          expect(allArgs.length).toEqual(1)
           expect(atlasEndpts).toContain(allArgs[0][0])
-          expect(atlasEndpts).toContain(allArgs[1][0])
         })
 
         it("> endpoint should be set", async () => {
@@ -67,11 +66,11 @@ describe("> sapi.service.ts", () => {
 
         it("> additional calls should return cached observable", () => {
 
-          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          expect(fetchSpy).toHaveBeenCalledTimes(1)
           SAPI.BsEndpoint$.subscribe()
           SAPI.BsEndpoint$.subscribe()
 
-          expect(fetchSpy).toHaveBeenCalledTimes(2)
+          expect(fetchSpy).toHaveBeenCalledTimes(1)
         })
       })
 
diff --git a/src/atlasComponents/sapi/sapi.service.ts b/src/atlasComponents/sapi/sapi.service.ts
index 7cd9209fb6299e521aebf324cc1e55265efbbfda..c3543e08c2ed155d20de261fc0eb74321dcf9bfd 100644
--- a/src/atlasComponents/sapi/sapi.service.ts
+++ b/src/atlasComponents/sapi/sapi.service.ts
@@ -5,13 +5,13 @@ import { getExportNehuba, noop } from "src/util/fn";
 import { MatSnackBar } from "@angular/material/snack-bar";
 import { AtlasWorkerService } from "src/atlasViewer/atlasViewer.workerService.service";
 import { EnumColorMapName } from "src/util/colorMaps";
-import { forkJoin, from, NEVER, Observable, of, throwError } from "rxjs";
+import { BehaviorSubject, forkJoin, from, NEVER, Observable, of, throwError } from "rxjs";
 import { environment } from "src/environments/environment"
 import {
   translateV3Entities
 } from "./translateV3"
 import { FeatureType, PathReturn, RouteParam, SapiRoute } from "./typeV3";
-import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature, Feature } from "./sxplrTypes";
+import { BoundingBox, SxplrAtlas, SxplrParcellation, SxplrRegion, SxplrTemplate, VoiFeature } from "./sxplrTypes";
 import { parcBanList, speciesOrder } from "src/util/constants";
 import { CONST } from "common/constants"
 
@@ -110,15 +110,6 @@ export class SAPI{
     
     BS_ENDPOINT_CACHED_VALUE = new Observable<string>(obs => {
       (async () => {
-        const backupPr = new Promise<string>(rs => {
-          for (const endpt of backupEndpoints) {
-            SAPI.VerifyEndpoint(endpt)
-              .then(flag => {
-                if (flag) rs(endpt)
-              })
-              .catch(noop)
-          }
-        })
         try {
           const url = await Promise.race([
             SAPI.VerifyEndpoint(mainEndpoint),
@@ -129,12 +120,23 @@ export class SAPI{
 
           try {
             const url = await Promise.race([
-              backupPr,
+              /**
+               * find the first endpoint of the backup endpoints
+               */
+              new Promise<string>(rs => {
+                for (const endpt of backupEndpoints) {
+                  SAPI.VerifyEndpoint(endpt)
+                    .then(flag => {
+                      if (flag) rs(endpt)
+                    })
+                    .catch(noop)
+                }
+              }),
               new Promise<string>((_, rj) => setTimeout(() => rj(`5s timeout`), 5000))
             ])
             obs.next(url)
           } catch (e) {
-            SAPI.ErrorMessage = `No usabe mirror found`
+            SAPI.ErrorMessage = `No usabe siibra-api endpoints found. Tried: ${mainEndpoint}, ${backupEndpoints.join(",")}`
           }
         } finally {
           obs.complete()
@@ -147,7 +149,10 @@ export class SAPI{
     return BS_ENDPOINT_CACHED_VALUE
   }
 
-  static ErrorMessage = null
+  static ErrorMessage$ = new BehaviorSubject<string>(null)
+  static set ErrorMessage(val: string){
+    this.ErrorMessage$.next(val)
+  }
 
   getParcRegions(parcId: string) {
     const param = {
@@ -187,40 +192,6 @@ export class SAPI{
     if (!!resp.total || resp.total === 0) return true
     return false
   }
-  getV3Features<T extends FeatureType>(featureType: T, sapiParam: RouteParam<`/feature/${T}`>): Observable<Feature[]> {
-    const query = structuredClone(sapiParam)
-    return this.v3Get<`/feature/${T}`>(`/feature/${featureType}`, {
-      ...query
-    }).pipe(
-      switchMap(resp => {
-        if (!this.#isPaged(resp)) return throwError(`endpoint not returning paginated response`)
-        return this.iteratePages(
-          resp,
-          page => {
-            const query = structuredClone(sapiParam)
-            query.query.page = page
-            return this.v3Get(`/feature/${featureType}`, {
-              ...query,
-            }).pipe(
-              map(val => {
-                if (this.#isPaged(val)) return val
-                return { items: [], total: 0, page: 0, size: 0 }
-              })
-            )
-          }
-        )
-      }),
-      switchMap(features => features.length === 0
-        ? of([])
-        : forkJoin(
-          features.map(feat => translateV3Entities.translateFeature(feat) )
-        )
-      ),
-      catchError((err) => {
-        console.error("Error fetching features", err)
-        return of([])}),
-    )
-  }
 
   getV3FeatureDetail<T extends FeatureType>(featureType: T, sapiParam: RouteParam<`/feature/${T}/{feature_id}`>): Observable<PathReturn<`/feature/${T}/{feature_id}`>> {
     return this.v3Get<`/feature/${T}/{feature_id}`>(`/feature/${featureType}/{feature_id}`, {
@@ -248,12 +219,6 @@ export class SAPI{
     )
   }
 
-  getModalities() {
-    return this.v3Get("/feature/_types", { query: {} }).pipe(
-      map(v => v.items)
-    )
-  }
-
   v3GetRoute<T extends SapiRoute>(route: T, sapiParam: RouteParam<T>) {
     const params: Record<string, string|number> = "query" in sapiParam ? sapiParam["query"] : {}
     const _path: Record<string, string|number> = "path" in sapiParam ? sapiParam["path"] : {}
@@ -593,9 +558,6 @@ export class SAPI{
     private snackbar: MatSnackBar,
     private workerSvc: AtlasWorkerService,
   ){
-    if (SAPI.ErrorMessage) {
-      this.snackbar.open(SAPI.ErrorMessage, 'Dismiss', { duration: 5000 })
-    }
   }
   
   /**
diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts
index 5fa7acf9251d3ced3358c2284d670b05ddaca300..bb0b092d7a025a191f6cced5c506b70a6a9d8143 100644
--- a/src/atlasComponents/userAnnotations/tools/service.ts
+++ b/src/atlasComponents/userAnnotations/tools/service.ts
@@ -2,12 +2,12 @@ import { Injectable, OnDestroy, Type } from "@angular/core";
 import { ARIA_LABELS } from 'common/constants'
 import { Inject, Optional } from "@angular/core";
 import { select, Store } from "@ngrx/store";
-import { BehaviorSubject, combineLatest, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs";
+import { BehaviorSubject, combineLatest, from, fromEvent, merge, Observable, of, Subject, Subscription } from "rxjs";
 import {map, switchMap, filter, shareReplay, pairwise, withLatestFrom } from "rxjs/operators";
 import { NehubaViewerUnit } from "src/viewerModule/nehuba";
 import { NEHUBA_INSTANCE_INJTKN } from "src/viewerModule/nehuba/util";
 import { AbsToolClass, ANNOTATION_EVENT_INJ_TOKEN, IAnnotationEvents, IAnnotationGeometry, INgAnnotationTypes, INJ_ANNOT_TARGET, TAnnotationEvent, ClassInterface, TCallbackFunction, TSands, TGeometryJson, TCallback, DESC_TYPE } from "./type";
-import { getExportNehuba, switchMapWaitFor } from "src/util/fn";
+import { getExportNehuba, switchMapWaitFor, retry } from "src/util/fn";
 import { Polygon } from "./poly";
 import { Line } from "./line";
 import { Point } from "./point";
@@ -455,14 +455,13 @@ export class ModularUserAnnotationToolService implements OnDestroy{
       store.pipe(
         select(atlasSelection.selectors.viewerMode),
         withLatestFrom(this.#voxelSize),
-      ).subscribe(([viewerMode, voxelSize]) => {
-        this.currMode = viewerMode
-        if (viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) {
-          if (this.annotationLayer) this.annotationLayer.setVisible(true)
-          else {
-            if (!voxelSize) throw new Error(`voxelSize of ${this.selectedTmpl.id} cannot be found!`)
+        switchMap(([viewerMode, voxelSize]) => from(
+          retry(() => {
             if (this.annotationLayer) {
-              this.annotationLayer.dispose()
+              return this.annotationLayer
+            }
+            if (!voxelSize) {
+              throw new Error(`voxelSize of ${this.selectedTmpl.id} cannot be found!`)
             }
             this.annotationLayer = new AnnotationLayer(
               ModularUserAnnotationToolService.ANNOTATION_LAYER_NAME,
@@ -479,15 +478,22 @@ export class ModularUserAnnotationToolService implements OnDestroy{
                   : null
               })
             })
-            /**
-             * on template changes, the layer gets lost
-             * force redraw annotations if layer needs to be recreated
-             */
-            this.forcedAnnotationRefresh$.next(null)
-          }
-        } else {
-          if (this.annotationLayer) this.annotationLayer.setVisible(false)
-        }
+            
+            return this.annotationLayer
+          })
+          ).pipe(
+            map(annotationLayer => ({viewerMode, annotationLayer}))
+          )
+        )
+      ).subscribe(({viewerMode, annotationLayer}) => {
+        this.currMode = viewerMode
+        
+        /**
+         * on template changes, the layer gets lost
+         * force redraw annotations if layer needs to be recreated
+         */
+        this.forcedAnnotationRefresh$.next(null)
+        annotationLayer.setVisible(viewerMode === ModularUserAnnotationToolService.VIEWER_MODE)
       })
     )
 
diff --git a/src/atlasViewer/atlasViewer.component.ts b/src/atlasViewer/atlasViewer.component.ts
index c127379a8ee112b36de6e104eaa8da3c62eb04b2..1fc4e3a606a4ae87698701fd2e0e60fb38f6ae75 100644
--- a/src/atlasViewer/atlasViewer.component.ts
+++ b/src/atlasViewer/atlasViewer.component.ts
@@ -27,6 +27,7 @@ import { DOCUMENT } from "@angular/common";
 import { userPreference } from "src/state"
 import { DARKTHEME } from "src/util/injectionTokens";
 import { EnumQuickTourSeverity } from "src/ui/quickTour/constrants";
+import { SAPI } from "src/atlasComponents/sapi";
 
 
 @Component({
@@ -44,6 +45,8 @@ export class AtlasViewer implements OnDestroy, OnInit, AfterViewInit {
 
   public CONST = CONST
 
+  sapiError$ = SAPI.ErrorMessage$
+
   @ViewChild('cookieAgreementComponent', {read: TemplateRef}) public cookieAgreementComponent: TemplateRef<any>
 
   @ViewChild(MouseHoverDirective) private mouseOverNehuba: MouseHoverDirective
diff --git a/src/atlasViewer/atlasViewer.template.html b/src/atlasViewer/atlasViewer.template.html
index 864976bc6901450056408aecc096df0c0254e7ce..cade469e7db2cd56594c21a6b6aa06f069722f48 100644
--- a/src/atlasViewer/atlasViewer.template.html
+++ b/src/atlasViewer/atlasViewer.template.html
@@ -1,7 +1,10 @@
 <ng-container *ngIf="meetsRequirement; else doesNotMeetReqTemplate">
 
-  <ng-container *ngTemplateOutlet="viewerBody">
-  </ng-container>
+  <ng-template [ngIf]="sapiError$ | async" let-data [ngIfElse]="viewerBody">
+    <ng-template [ngTemplateOutlet]="errorTmpl" [ngTemplateOutletContext]="{ message: data }">
+    </ng-template>
+  </ng-template>
+
 </ng-container>
 
 <!-- cookie -->
@@ -56,3 +59,8 @@
 
 <ng-template #emptyArrowTmpl>
 </ng-template>
+
+<ng-template #errorTmpl let-message="message">
+  <not-supported-component [errorString]="message">
+  </not-supported-component>
+</ng-template>
diff --git a/src/features/category-acc.directive.ts b/src/features/category-acc.directive.ts
index 73156e7af17e6cf4e39e97fca101655eb02b07bb..a6f34d27d9b68532408780eb774ecb707718fcdf 100644
--- a/src/features/category-acc.directive.ts
+++ b/src/features/category-acc.directive.ts
@@ -144,6 +144,12 @@ export class CategoryAccDirective implements AfterContentInit, OnDestroy {
       ).subscribe(async ({ total, current, ds }) => {
         if (total > current && current < 50) {
           try {
+            /**
+             * TODO Interaction between ParentDataSource and ListDatadirective, which both pulls seems
+             * to weirdly interact with each other. 
+             * For now, pulling twice seems to solve the issue
+             */
+            await ds.pull()
             await ds.pull()
           } catch (e) {
             // if already pulling, ignore
diff --git a/src/notSupportedCmp/notSupported.component.ts b/src/notSupportedCmp/notSupported.component.ts
index 6562a46a650f2c2c8b01caa6b1fc174d7251157f..f17b70e3fa288c434c047c76efc2f201e621105b 100644
--- a/src/notSupportedCmp/notSupported.component.ts
+++ b/src/notSupportedCmp/notSupported.component.ts
@@ -1,4 +1,4 @@
-import { Component } from "@angular/core";
+import { Component, Input } from "@angular/core";
 import { interval, merge, of } from "rxjs";
 import { map } from "rxjs/operators";
 import { UNSUPPORTED_INTERVAL, UNSUPPORTED_PREVIEW } from "src/util/constants";
@@ -13,6 +13,14 @@ import { MIN_REQ_EXPLAINER } from 'src/util/constants'
 })
 
 export class NotSupportedCmp{
+
+  /**
+   * default error is webgl
+   * custom error message can be inputed to override default message
+   */
+  @Input()
+  errorString="webgl"
+
   public unsupportedPreviews: any[] = UNSUPPORTED_PREVIEW
   public unsupportedPreviewIdx: number = 0
   public MIN_REQ_EXPLAINER = MIN_REQ_EXPLAINER
diff --git a/src/notSupportedCmp/notSupported.template.html b/src/notSupportedCmp/notSupported.template.html
index db0d8020b24ace07d3e798df155ba118074aaecb..c46e667a4c2d8b8a40496cccc1a36f2e22afdad1 100644
--- a/src/notSupportedCmp/notSupported.template.html
+++ b/src/notSupportedCmp/notSupported.template.html
@@ -1,5 +1,5 @@
 <div class="jumbotron bg-light text-center mb-0">
-  <div>
+  <div *ngIf="errorString === 'webgl' else otherErrorTmpl">
     <h1 class="mb-3">
       <i class="fas fa-exclamation-triangle"></i> Unsupported browser detected
     </h1>
@@ -21,6 +21,12 @@
     </div>
 
   </div>
+
+  <ng-template #otherErrorTmpl>
+    <h1 class="mb-3">
+      <i class="fas fa-exclamation-triangle"></i> {{ errorString }}
+    </h1>
+  </ng-template>
 </div>
 <ng-container *ngFor="let preview of unsupportedPreviews; let idx = index">
   <div [hidden]="idx !== unsupportedPreviewIdx" class="text-center mb-3 image-container"
@@ -31,4 +37,4 @@
       </div>
     </div>
   </div>
-</ng-container>
\ No newline at end of file
+</ng-container>
diff --git a/src/util/fn.ts b/src/util/fn.ts
index 45a4f4be56e38b52994c0d40319fa98ceef0ae2b..1508dc6303509477d45e744e28f23d5e1ae9d1eb 100644
--- a/src/util/fn.ts
+++ b/src/util/fn.ts
@@ -5,6 +5,21 @@ import { filter, mapTo, take } from 'rxjs/operators'
 // eslint-disable-next-line  @typescript-eslint/no-empty-function
 export function noop(){}
 
+export async function retry<T>(fn: () => T, config={timeout: 1000, retries:3}){
+  let retryNo = 0
+  const { retries, timeout } = config
+  while (retryNo < retries) {
+    retryNo ++
+    try {
+      return await fn()
+    } catch (e) {
+      console.warn(`fn failed, retry after ${timeout} milliseconds`)
+      await (() => new Promise(rs => setTimeout(rs, timeout)))()
+    }
+  }
+  throw new Error(`fn failed ${retries} times, aborting`)
+}
+
 export async function getExportNehuba() {
   // eslint-disable-next-line no-constant-condition
   while (true) {
diff --git a/src/util/pullable.ts b/src/util/pullable.ts
index 6a5d87e96701b3c9c41553ce66c0f611054c0afa..02bf6dc4481a62c9189e90601781c885f1451623 100644
--- a/src/util/pullable.ts
+++ b/src/util/pullable.ts
@@ -70,8 +70,14 @@ export class PulledDataSource<T> extends DataSource<T> {
       return []
     }
     this.isPulling = true
-    const newResults = await this.#pull()
-    this.isPulling = false
+    let newResults = []
+    try {
+      newResults = await this.#pull()
+    } catch (e) {
+      console.error("Pulling failed", e)
+    } finally {
+      this.isPulling = false
+    }
     if (newResults.length === 0) {
       this.complete()
     }
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
index be87e8eeec2a8fad91596feadac14a1dde29da3f..dffa37da9a7f919967e83b9a3af061b1d3ce72a9 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.service.ts
@@ -397,4 +397,22 @@ export class NehubaLayerControlService implements OnDestroy{
   ]).pipe(
     map(([ expectedLayerNames, customLayerNames, pmapName ]) => [...expectedLayerNames, ...customLayerNames, ...pmapName, ...AnnotationLayer.Map.keys()])
   )
+
+
+  static ExternalLayerNames = new Set<string>()
+
+  /**
+   * @description Occationally, a layer can be managed by external components. Register the name of such layers so it will be ignored.
+   * @param layername 
+   */
+  static RegisterLayerName(layername: string) {
+    NehubaLayerControlService.ExternalLayerNames.add(layername)
+  }
+  /**
+   * @description Once external component is done with the layer, return control back to the service
+   * @param layername 
+   */
+  static DeregisterLayerName(layername: string) {
+    NehubaLayerControlService.ExternalLayerNames.delete(layername)
+  }
 }
diff --git a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
index 1e18596e98fe06959628aa9186d2e2dd08981f98..d0e9069d3a17e0520ba2a4202ac3c20b132d01cb 100644
--- a/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
+++ b/src/viewerModule/nehuba/layerCtrl.service/layerCtrl.util.ts
@@ -61,12 +61,19 @@ export type TNgLayerCtrl<T extends keyof INgLayerCtrl> = {
   payload: INgLayerCtrl[T]
 }
 
+export interface IExternalLayerCtl {
+  RegisterLayerName(layername: string): void
+  DeregisterLayerName(layername: string): void
+  readonly ExternalLayerNames: Set<string>
+}
+
 export const SET_COLORMAP_OBS = new InjectionToken<Observable<IColorMap>>('SET_COLORMAP_OBS')
 export const SET_LAYER_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_LAYER_VISIBILITY')
 export const SET_SEGMENT_VISIBILITY = new InjectionToken<Observable<string[]>>('SET_SEGMENT_VISIBILITY')
 export const NG_LAYER_CONTROL = new InjectionToken<TNgLayerCtrl<keyof INgLayerCtrl>>('NG_LAYER_CONTROL')
 export const Z_TRAVERSAL_MULTIPLIER = new InjectionToken<Observable<number>>('Z_TRAVERSAL_MULTIPLIER')
 export const CURRENT_TEMPLATE_DIM_INFO = new InjectionToken<Observable<TemplateInfo>>('CURRENT_TEMPLATE_DIM_INFO')
+export const EXTERNAL_LAYER_CONTROL = new InjectionToken<IExternalLayerCtl>("EXTERNAL_LAYER_CONTROL")
 
 export type TemplateInfo = {
   transform: number[][]
diff --git a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
index 2a06e3f2d32b1427342a14e2f7f9480085cc506a..3bede3ea12c10cb8c7695cbef9a3ecfe1e966b0d 100644
--- a/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewer/nehubaViewer.component.ts
@@ -11,7 +11,7 @@ import { IColorMap, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.
 /**
  * import of nehuba js files moved to angular.json
  */
-import { INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util";
+import { EXTERNAL_LAYER_CONTROL, IExternalLayerCtl, INgLayerCtrl, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY, TNgLayerCtrl, Z_TRAVERSAL_MULTIPLIER } from "../layerCtrl.service/layerCtrl.util";
 import { NgCoordinateSpace, Unit } from "../types";
 import { PeriodicSvc } from "src/util/periodic.service";
 import { ViewerInternalStateSvc, AUTO_ROTATE } from "src/viewerModule/viewerInternalState.service";
@@ -131,6 +131,7 @@ export class NehubaViewerUnit implements OnDestroy {
     @Optional() @Inject(SET_SEGMENT_VISIBILITY) private segVis$: Observable<string[]>,
     @Optional() @Inject(NG_LAYER_CONTROL) private layerCtrl$: Observable<TNgLayerCtrl<keyof INgLayerCtrl>>,
     @Optional() @Inject(Z_TRAVERSAL_MULTIPLIER) multiplier$: Observable<number>,
+    @Optional() @Inject(EXTERNAL_LAYER_CONTROL) private externalLayerCtrl: IExternalLayerCtl,
     @Optional() intViewerStateSvc: ViewerInternalStateSvc,
   ) {
     if (multiplier$) {
@@ -261,7 +262,12 @@ export class NehubaViewerUnit implements OnDestroy {
            * on switch from freesurfer -> volumetric viewer, race con results in managed layer not necessarily setting layer visible correctly
            */
           const managedLayers = this.nehubaViewer.ngviewer.layerManager.managedLayers
-          managedLayers.forEach(layer => layer.setVisible(false))
+          managedLayers.forEach(layer => {
+            if (this.externalLayerCtrl && this.externalLayerCtrl.ExternalLayerNames.has(layer.name)) {
+              return
+            }
+            layer.setVisible(false)
+          })
           
           for (const layerName of layerNames) {
             const layer = this.nehubaViewer.ngviewer.layerManager.getLayerByName(layerName)
diff --git a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
index c3bc899bd568f5242f37f4a90f2cf1f57f78579b..1f47b5680c88ac1c21faeca6a89104d4f3e54ce7 100644
--- a/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
+++ b/src/viewerModule/nehuba/nehubaViewerGlue/nehubaViewerGlue.component.ts
@@ -5,7 +5,7 @@ import { distinctUntilChanged } from "rxjs/operators";
 import { IViewer, TViewerEvent } from "../../viewer.interface";
 import { NehubaMeshService } from "../mesh.service";
 import { NehubaLayerControlService, SET_COLORMAP_OBS, SET_LAYER_VISIBILITY } from "../layerCtrl.service";
-import { NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util";
+import { EXTERNAL_LAYER_CONTROL, NG_LAYER_CONTROL, SET_SEGMENT_VISIBILITY } from "../layerCtrl.service/layerCtrl.util";
 import { SxplrRegion } from "src/atlasComponents/sapi/sxplrTypes";
 import { NehubaConfig } from "../config.service";
 import { SET_MESHES_TO_LOAD } from "../constants";
@@ -25,6 +25,10 @@ import { atlasSelection, userInteraction } from "src/state";
       useFactory: (meshService: NehubaMeshService) => meshService.loadMeshes$,
       deps: [ NehubaMeshService ]
     },
+    {
+      provide: EXTERNAL_LAYER_CONTROL,
+      useValue: NehubaLayerControlService
+    },
     NehubaMeshService,
     {
       provide: SET_COLORMAP_OBS,