diff --git a/deploy/auth/hbp-oidc-v2.js b/deploy/auth/hbp-oidc-v2.js
index 102aa25c236b6a61801fcae04541622af23ea676..d5427480817ab9ad0a58e9a2c2a103eafa6d5f3b 100644
--- a/deploy/auth/hbp-oidc-v2.js
+++ b/deploy/auth/hbp-oidc-v2.js
@@ -78,8 +78,4 @@ module.exports = {
       console.error('oidcv2 auth error', e)
     }
   },
-  getClient: async () => {
-    await memoizedInit()
-    return client
-  }
 }
diff --git a/deploy/auth/index.spec.js b/deploy/auth/index.spec.js
index 84691ddd6ccae9169f321858453c2eacf0dcb28c..8e33c30bd02054d71c31b862b22928d12f359928 100644
--- a/deploy/auth/index.spec.js
+++ b/deploy/auth/index.spec.js
@@ -8,7 +8,7 @@ const appGetStub = sinon.stub()
 describe('auth/index.js', () => {
   before(() => {
     require.cache[require.resolve('./util')] = {
-      exports: { initPassportJs: initPassportJsStub }
+      exports: { initPassportJs: initPassportJsStub, objStoreDb: new Map() }
     }
     require.cache[require.resolve('./hbp-oidc-v2')] = {
       exports: {
diff --git a/deploy/package-lock.json b/deploy/package-lock.json
index 3a8058b86a578efd909ca7b5d28a32edd93e5492..b8399147d5ab12a520ea0f2068cba33945db6f9b 100644
--- a/deploy/package-lock.json
+++ b/deploy/package-lock.json
@@ -503,6 +503,11 @@
         "wrap-ansi": "^2.0.0"
       }
     },
+    "clone": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
+      "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
+    },
     "clone-response": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz",
@@ -695,6 +700,14 @@
         "type-detect": "^4.0.0"
       }
     },
+    "defaults": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
+      "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=",
+      "requires": {
+        "clone": "^1.0.2"
+      }
+    },
     "defer-to-connect": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz",
@@ -936,6 +949,11 @@
         }
       }
     },
+    "express-rate-limit": {
+      "version": "5.5.1",
+      "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.5.1.tgz",
+      "integrity": "sha512-MTjE2eIbHv5DyfuFz4zLYWxpqVhEhkTiwFGuB74Q9CSou2WHO52nlE5y3Zlg6SIsiYUIPj6ifFxnkPz6O3sIUg=="
+    },
     "express-session": {
       "version": "1.15.6",
       "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.15.6.tgz",
@@ -2118,6 +2136,15 @@
       "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
       "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
     },
+    "rate-limit-redis": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/rate-limit-redis/-/rate-limit-redis-2.1.0.tgz",
+      "integrity": "sha512-6SAsTCzY0v6UCIKLOLLYqR2XzFmgdtF7jWXlSPq2FrNIZk8tZ7xwBvyGW7GFMCe5I4S9lYNdrSJ9E84rz3/CpA==",
+      "requires": {
+        "defaults": "^1.0.3",
+        "redis": "^3.0.2"
+      }
+    },
     "raw-body": {
       "version": "2.4.0",
       "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
diff --git a/deploy/package.json b/deploy/package.json
index a8b5d1488439346a9c7b6adc2322a57a482f218e..04cdc908c505ae7a5d8663fbfae313f561b0be80 100644
--- a/deploy/package.json
+++ b/deploy/package.json
@@ -17,6 +17,7 @@
     "connect-redis": "^5.0.0",
     "cookie-parser": "^1.4.5",
     "express": "^4.16.4",
+    "express-rate-limit": "^5.5.1",
     "express-session": "^1.15.6",
     "got": "^10.5.5",
     "hbp-seafile": "^0.2.0",
@@ -26,6 +27,7 @@
     "nomiseco": "0.0.2",
     "openid-client": "^4.4.0",
     "passport": "^0.4.0",
+    "rate-limit-redis": "^2.1.0",
     "redis": "^3.1.2",
     "request": "^2.88.0",
     "showdown": "^1.9.1",
diff --git a/deploy/saneUrl/depcObjStore.js b/deploy/saneUrl/depcObjStore.js
index a46d6c845936fef10b77b4ac9bb9ed278c8a00e9..c67876686470650d08217cc72141a6936f703511 100644
--- a/deploy/saneUrl/depcObjStore.js
+++ b/deploy/saneUrl/depcObjStore.js
@@ -21,6 +21,10 @@ class Store {
     })
   }
 
+  async del(id){
+    // noop
+  }
+
   async set(id, val){
     throw new Error(`Object store is deprecated. Please use seafile storage instead`)
   }
diff --git a/deploy/saneUrl/index.js b/deploy/saneUrl/index.js
index 09fe10b25a57e336b25118d57f4b5849c1f8234d..2e9ab0b76528b999720781f7ff9cc913891f478d 100644
--- a/deploy/saneUrl/index.js
+++ b/deploy/saneUrl/index.js
@@ -1,46 +1,33 @@
 const express = require('express')
 const router = express.Router()
-const { FallbackStore: Store, NotFoundError } = require('./store')
+const { GitlabSnippetStore: Store, NotFoundError } = require('./store')
 const { Store: DepcStore } = require('./depcObjStore')
+const RateLimit = require('express-rate-limit')
+const RedisStore = require('rate-limit-redis')
+const { redisURL } = require('../lruStore')
+const { ProxyStore, NotExactlyPromiseAny } = require('./util')
 
 const store = new Store()
 const depStore = new DepcStore()
 
-const { HOSTNAME, HOST_PATHNAME } = process.env
+const proxyStore = new ProxyStore(store)
 
-const acceptHtmlProg = /text\/html/i
-
-const getFileFromStore = async (name, store) => {
-  try {
-    const value = await store.get(name)
-    const json = JSON.parse(value)
-    const { expiry } = json
-    if ( expiry && ((Date.now() - expiry) > 0) ) {
-      return null
-    }
-  
-    return value
-  } catch (e) {
-    if (e instanceof NotFoundError) {
-      return null
-    }
-    throw e
-  }
-}
+const {
+  HOSTNAME,
+  HOST_PATHNAME,
+  DISABLE_LIMITER,
+} = process.env
 
-const getFile = async name => {
-  const value = await getFileFromStore(name, depStore)
-    || await getFileFromStore(name, store)
+const limiter = new RateLimit({
+  windowMs: 1e3 * 5,
+  max: 5,
+  ...( redisURL ? { store: new RedisStore({ redisURL }) } : {} )
+})
+const passthrough = (_, __, next) => next()
 
-  return value
-}
+const acceptHtmlProg = /text\/html/i
 
-const hardCodedMap = new Map([
-  ['bigbrainGreyWhite', '#/a:juelich:iav:atlas:v1.0.0:1/t:minds:core:referencespace:v1.0.0:a1655b99-82f1-420f-a3c2-fe80fd4c8588/p:juelich:iav:atlas:v1.0.0:4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..gIW~.10AwC.B1KK~..1LSm'],
-  ['whs4', '#/a:minds:core:parcellationatlas:v1.0.0:522b368e-49a3-49fa-88d3-0870a307974a/t:minds:core:referencespace:v1.0.0:d5717c4a-0fa1-46e6-918c-b8003069ade8/p:minds:core:parcellationatlas:v1.0.0:ebb923ba-b4d5-4b82-8088-fa9215c2e1fe-v4/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..kxV..0.0.0..8Yu'],
-  ['allen2017', '#/a:juelich:iav:atlas:v1.0.0:2/t:minds:core:referencespace:v1.0.0:265d32a0-3d84-40a5-926f-bf89f68212b9/p:minds:core:parcellationatlas:v1.0.0:05655b58-3b6f-49db-b285-64b5a0276f83/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..kxV..0.0.0..8Yu'],
-  ['mebrains', '#/a:juelich:iav:atlas:v1.0.0:monkey/t:minds:core:referencespace:v1.0.0:MEBRAINS_T1.masked/p:minds:core:parcellationatlas:v1.0.0:mebrains-tmp-id/@:0.0.0.-W000.._eCwg.2-FUe3._-s_W.2_evlu..7LIx..0.0.0..1LSm']
-])
+const REAL_HOSTNAME = `${HOSTNAME}${HOST_PATHNAME || ''}/`
 
 router.get('/:name', async (req, res) => {
   const { name } = req.params
@@ -49,31 +36,29 @@ router.get('/:name', async (req, res) => {
   const redirectFlag = acceptHtmlProg.test(headers['accept'])
     
   try {
-    const REAL_HOSTNAME = `${HOSTNAME}${HOST_PATHNAME || ''}/`
-    const hardcodedRedir = hardCodedMap.get(name)
-    if (hardcodedRedir) {
-      if (redirectFlag) res.redirect(`${REAL_HOSTNAME}${hardcodedRedir}`)
-      else res.status(200).send(hardcodedRedir)
-      return
-    }
-
-    const value = await getFile(name)
-    if (!value) throw new NotFoundError()
-    const json = JSON.parse(value)
-    const { queryString } = json
 
+    const json = await NotExactlyPromiseAny([
+      ProxyStore.StaticGet(depStore, req, name),
+      proxyStore.get(req, name)
+    ])
 
-    if (redirectFlag) res.redirect(`${REAL_HOSTNAME}?${queryString}`)
-    else res.status(200).send(value)
+    const { queryString, hashPath } = json
 
+    if (redirectFlag) {
+      if (queryString) return res.redirect(`${REAL_HOSTNAME}?${queryString}`)
+      if (hashPath) return res.redirect(`${REAL_HOSTNAME}#${hashPath}`)
+    } else {
+      return res.status(200).send(json)
+    }
   } catch (e) {
+    const notFoundFlag = e instanceof NotFoundError
     if (redirectFlag) {
 
       const REAL_HOSTNAME = `${HOSTNAME}${HOST_PATHNAME || ''}/`
 
       res.cookie(
         'iav-error', 
-        e instanceof NotFoundError ? `${name} 
+        notFoundFlag ? `${name} 
         
         not found` : `error while fetching ${name}.`,
         {
@@ -84,13 +69,25 @@ router.get('/:name', async (req, res) => {
       )
       return res.redirect(REAL_HOSTNAME)
     }
-    if (e instanceof NotFoundError) return res.status(404).end()
+    if (notFoundFlag) return res.status(404).end()
     else return res.status(500).send(e.toString())
   }
 })
 
 router.post('/:name',
-  (_req, res) => res.status(410).end()
+  DISABLE_LIMITER ? passthrough : limiter,
+  express.json(),
+  async (req, res) => {
+    if (req.headers['x-noop']) return res.status(200).end()
+    const { name } = req.params
+    try {
+      await proxyStore.set(req, name, req.body)
+      res.status(201).end()
+    } catch (e) {
+      console.log(e.body)
+      res.status(500).send(e.toString())
+    }
+  }
 )
 
 router.use((_, res) => {
diff --git a/deploy/saneUrl/store.js b/deploy/saneUrl/store.js
index 656a070f4fcd5805d816c54390449546fa2885a1..9ef35e8408fd089bb7778ed02d68bd3e18fede58 100644
--- a/deploy/saneUrl/store.js
+++ b/deploy/saneUrl/store.js
@@ -1,420 +1,112 @@
-const redis = require('redis')
-const { promisify } = require('util')
-const { getClient } = require('../auth/hbp-oidc-v2')
-const { jwtDecode } = require('../auth/oidc')
 const request = require('request')
 
-const HBP_OIDC_V2_REFRESH_TOKEN_KEY = `HBP_OIDC_V2_REFRESH_TOKEN_KEY` // only insert valid refresh token. needs to be monitored to ensure always get new refresh token(s)
-const HBP_OIDC_V2_ACCESS_TOKEN_KEY = `HBP_OIDC_V2_ACCESS_TOKEN_KEY` // only insert valid access token. if expired, get new one via refresh token, then pop & push
-const HBP_OIDC_V2_UPDATE_CHAN = `HBP_OIDC_V2_UPDATE_CHAN` // stringified JSON key val of above three, with time stamp
-const HBP_OIDC_V2_UPDATE_KEY = `HBP_OIDC_V2_UPDATE_KEY`
-const HBP_DATAPROXY_READURL = `HBP_DATAPROXY_READURL`
-const HBP_DATAPROXY_WRITEURL = `HBP_DATAPROXY_WRITEURL`
+const apiPath = '/api/v4'
+const saneUrlVer = `0.0.1`
+const titlePrefix = `[saneUrl]`
 
 const {
   __DEBUG__,
-
-  HBP_V2_REFRESH_TOKEN,
-  HBP_V2_ACCESS_TOKEN,
-
-  DATA_PROXY_URL,
-  DATA_PROXY_BUCKETNAME,
-
+  GITLAB_ENDPOINT,
+  GITLAB_PROJECT_ID,
+  GITLAB_TOKEN
 } = process.env
 
 class NotFoundError extends Error{}
 
-// not part of class, since class is exported, and prototype of class can be easily changed
-// using stderr instead of console.error, as by default, logs are collected via fluentd
-// debug messages should not be kept as logs
-function log(message){
-  if (__DEBUG__) {
-    process.stderr.write(`__DEBUG__`)
-    process.stdout.write(`\n`)
-    if (typeof message === 'object') {
-      process.stderr.write(JSON.stringify(message, null, 2))
-    }
-    if (typeof message === 'number' || typeof message === 'string') {
-      process.stdout.write(message)
-    }
-    process.stdout.write(`\n`)
-  }
-}
-
-
-function _checkValid(urlString){
-  log({
-    breakpoint: '_checkValid',
-    payload: urlString
-  })
-  if (!urlString) return false
-  const url = new URL(urlString)
-  const expiry = url.searchParams.get('temp_url_expires')
-  return (new Date() - new Date(expiry)) < 1e3 * 10
-}
-
-class FallbackStore {
-  async get(){
-    throw new Error(`saneurl is currently offline for maintainence.`)
-  }
-  async set(){
-    throw new Error(`saneurl is currently offline for maintainence.`)
-  }
-  async healthCheck() {
-    return false
-  }
-}
+class NotImplemented extends Error{}
 
-class Store {
+class GitlabSnippetStore {
   constructor(){
-    throw new Error(`Not implemented`)
-
-    this.healthFlag = false
-    this.seafileHandle = null
-    this.seafileRepoId = null
-
-    /**
-     * setup redis(or mock) client
-     */
-    this.redisClient = redisURL
-      ? redis.createClient({
-          url: redisURL
-        })
-      : ({
-          onCbs: [],
-          keys: {},
-          get: async (key, cb) => {
-            await Promise.resolve()
-            cb(null, this.keys[key])
-          },
-          set: async (key, value, cb) => {
-            await Promise.resolve()
-            this.keys[key] = value
-            cb(null)
-          },
-          on(eventName, cb) {
-            if (eventName === 'message') {
-              this.onCbs.push(cb)
-            }
-          },
-          publish(channel, message){
-            for (const cb of this.onCbs){
-              cb(channel, message)
-            }
-          },
-          quit(){}
-        })
-
-    this.redisUtil = {
-      asyncGet: promisify(this.redisClient.get).bind(this.redisClient),
-      asyncSet: promisify(this.redisClient.set).bind(this.redisClient),
-    }
-
-    this.pending = {}
-    this.keys = {}
-
-    this.redisClient.on('message', async (chan, mess) => {
-      /**
-       * only liten to HBP_OIDC_V2_UPDATE_CHAN update
-       */
-      if (chan === HBP_OIDC_V2_UPDATE_CHAN) {
-        try {
-          const { pending, update } = JSON.parse(mess)
-          this.pending = pending
-          for (const key in update) {
-            try {
-              this.keys[key] = await this.redisUtil.asyncGet(key)
-              console.log('on message get key', key, this.keys[key])
-            } catch (e) {
-              console.error(`[saneUrl][store.js] get key ${key} error`)
-            }
-          }
-        } catch (e) {
-          console.error(`[saneUrl][store.js] parse message HBP_OIDC_V2_UPDATE_CHAN error`)
-        }
-      }
-    })
     
-    this.init()
-
-    /**
-     * check expiry
-     */
-    this.intervalRef = setInterval(() => {
-      this.checkExpiry()
-    }, 1000 * 60)
-  }
-
-  async init() {
-    this.keys = {
-      [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: (await this.redisUtil.asyncGet(HBP_OIDC_V2_REFRESH_TOKEN_KEY)) || HBP_V2_REFRESH_TOKEN,
-      [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: (await this.redisUtil.asyncGet(HBP_OIDC_V2_ACCESS_TOKEN_KEY)) || HBP_V2_ACCESS_TOKEN,
-      [HBP_DATAPROXY_READURL]: await this.redisUtil.asyncGet(HBP_DATAPROXY_READURL),
-      [HBP_DATAPROXY_WRITEURL]: await this.redisUtil.asyncGet(HBP_DATAPROXY_WRITEURL),
-    }
-    
-    this.healthFlag = true
-  }
-
-  async checkExpiry(){
-
-    const {
-      [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: refreshToken,
-      [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: accessToken
-    } = this.keys
-
-    log({
-      breakpoint: 'async checkExpiry',
-    })
-
-    /**
-     * if access token is absent
-     * try to refresh token, without needing to check exp
-     */
-    if (!accessToken) {
-      await this.doRefreshTokens()
-      return true
-    }
-    const { exp: refreshExp } = jwtDecode(refreshToken)
-    const { exp: accessExp } = jwtDecode(accessToken)
-
-    const now = new Date().getTime() / 1000
-
-    if (now > refreshExp) {
-      console.warn(`[saneUrl] refresh token expired... Need a new refresh token`)
-      return false
-    }
-    if (now > accessExp) {
-      console.log(`[saneUrl] access token expired. Refreshing access token...`)
-      await this.doRefreshTokens()
-      console.log(`[saneUrl] access token successfully refreshed...`)
-      return true
+    if (
+      GITLAB_ENDPOINT
+      && GITLAB_PROJECT_ID
+      && GITLAB_TOKEN
+    ) {
+      this.url = `${GITLAB_ENDPOINT}${apiPath}/projects/${GITLAB_PROJECT_ID}/snippets`
+      this.token = GITLAB_TOKEN
+      return
     }
-
-    return true
+    throw new NotImplemented('Gitlab snippet key value store cannot be configured')
   }
 
-  async doRefreshTokens(){
-
-    log({
-      breakpoint: 'doing refresh tokens'
+  _promiseRequest(...arg) {
+    return new Promise((rs, rj) => {
+      request(...arg, (err, resp, body) => {
+        if (err) return rj(err)
+        if (resp.statusCode >= 400) return rj(resp)
+        rs(body)
+      })
     })
-
-    /**
-     * first, check if another process/pod is currently updating
-     * if they are, give them 1 minute to complete the process
-     * (usually takes takes less than 5 seconds)
-     */
-    const pendingStart = this.pending[HBP_OIDC_V2_REFRESH_TOKEN_KEY] &&
-      this.pending[HBP_OIDC_V2_REFRESH_TOKEN_KEY].start + 1000 * 60
-    const now = new Date() 
-    if (pendingStart && pendingStart > now) {
-      return
-    }
-
-    /**
-     * When start refreshing the tokens, set pending attribute, and start timestamp
-     */
-    const payload = {
-      pending: {
-        ...this.pending,
-        [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: {
-          start: Date.now()
-        }
-      }
-    }
-    await this.redisUtil.asyncSet(HBP_OIDC_V2_UPDATE_KEY, JSON.stringify(payload))
-    this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify(payload))
-
-    const client = await getClient()
-    const tokenset = await client.refresh(this.keys[HBP_OIDC_V2_REFRESH_TOKEN_KEY])
-    const { access_token: accessToken, refresh_token: refreshToken } = tokenset
-
-    const { exp: accessTokenExp } = jwtDecode(accessToken)
-    const { exp: refreshTokenExp } = jwtDecode(refreshToken)
-
-    if (refreshTokenExp - accessTokenExp < 60 * 60 ) {
-      console.warn(`[saneUrl] refreshToken expires within 1 hour of access token! ${accessTokenExp} ${refreshTokenExp}`)
-    }
-
-    /**
-     * once tokens have been refreshed, set them in the redis store first
-     * Then publish the update message
-     */
-    await this.redisUtil.asyncSet(HBP_OIDC_V2_REFRESH_TOKEN_KEY, refreshToken)
-    await this.redisUtil.asyncSet(HBP_OIDC_V2_ACCESS_TOKEN_KEY, accessToken)
-    this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify({
-      keys: {
-        [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: refreshToken,
-        [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: accessToken,
-      },
-      pending: {
-        ...this.pending,
-        [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: null
-      }
-    }))
-    this.keys = {
-      [HBP_OIDC_V2_REFRESH_TOKEN_KEY]: refreshToken,
-      [HBP_OIDC_V2_ACCESS_TOKEN_KEY]: accessToken,
-    }
   }
 
-  async _getTmpUrl(permission){
-    if (permission !== 'read' && permission !== 'write') throw new Error(`permission need to be either read or write!`)
-    const method = permission === 'read'
-      ? 'GET'
-      : 'PUT'
-    log({
-      breakpoint: 'access key',
-      access: this.keys[HBP_OIDC_V2_ACCESS_TOKEN_KEY]
-    })
-    const { url } = await new Promise((rs, rj) => {
-      const payload = {
+  _request({ addPath = '', method = 'GET', headers = {}, opt = {} } = {}) {
+    return new Promise((rs, rj) => {
+      request(`${this.url}${addPath}`, {
         method,
-        uri: `${DATA_PROXY_URL}/tempurl/${DATA_PROXY_BUCKETNAME}?lifetime=very_long`,
         headers: {
-          'Authorization': `Bearer ${this.keys[HBP_OIDC_V2_ACCESS_TOKEN_KEY]}`
-        }
-      }
-      log({
-        breakpoint: '_getTmpUrl',
-        payload
-      })
-      request(payload, (err, resp, body) => {
+          'PRIVATE-TOKEN': this.token,
+          ...headers
+        },
+        ...opt
+      }, (err, resp, body) => {
         if (err) return rj(err)
-        if (resp.statusCode >= 400) return rj(new Error(`${resp.statusCode}: ${resp.statusMessage}`))
-        if (resp.statusCode === 200) {
-          rs(JSON.parse(body))
-          return
-        }
-        return rj(new Error(`[saneurl] [get] unknown error`))
+        if (resp.statusCode >= 400) return rj(resp)
+        return rs(body)
       })
     })
-
-    return url
-  }
-
-  _getTmplTag(key, _strings, _proxyUrl, bucketName, id){
-    const defaultReturn = `${_strings[0]}${_proxyUrl}${_strings[1]}${bucketName}${_strings[2]}${id}`
-    if (!key) return defaultReturn
-    const {
-      [key]: urlOfInterest
-    } = this.keys
-    if (!urlOfInterest) return defaultReturn
-    const url = new URL(urlOfInterest)
-    url.pathname += id
-    return url
-  }
-
-  _getWriteTmplTag(...args) {
-    return this._getTmplTag(HBP_DATAPROXY_WRITEURL, ...args)
-  }
-
-  _getReadTmplTag(...args){
-    return this._getTmplTag(HBP_DATAPROXY_READURL, ...args)
   }
 
   async get(id) {
-    log({
-      breakpoint: 'async get',
-      id
-    })
-    const {
-      [HBP_DATAPROXY_READURL]: readUrl
-    } = this.keys
-
-    if (!_checkValid(readUrl)) {
-      log({
-        breakpoint: 'read url not valid, getting new url'
-      })
-      const url = await this._getTmpUrl('read')
-      const payload = {
-        keys: {
-          [HBP_DATAPROXY_READURL]: url
-        }
-      }
-      this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify(payload))
-      this.redisUtil.asyncSet(HBP_DATAPROXY_READURL, url)
-      this.keys[HBP_DATAPROXY_READURL] = url
+    const list = JSON.parse(await this._request())
+    const found = list.find(item => item.title === `${titlePrefix}${id}`)
+    if (!found) throw new NotFoundError()
+    const payloadObj = found.files.find(f => f.path === 'payload')
+    if (!payloadObj) {
+      console.error(`id found, but payload not found... this is strange. Check id: ${id}`)
+      throw new NotFoundError()
     }
-
-    return await new Promise((rs, rj) => {
-      request({
-        method: 'GET',
-        uri: this._getReadTmplTag`${DATA_PROXY_URL}/buckets/${DATA_PROXY_BUCKETNAME}/${encodeURIComponent(id)}`,
-      }, (err, resp, body) => {
-        if (err) return rj(err)
-        if (resp.statusCode === 404) return rj(new NotFoundError())
-        if (resp.statusCode >= 400) return rj(new Error(`${resp.statusCode}: ${resp.statusMessage}`))
-        if (resp.statusCode === 200) return rs(body)
-        return rj(new Error(`[saneurl] [get] unknown error`))
-      })
-    })
+    if (!payloadObj.raw_url) {
+      throw new Error(`payloadObj.raw_url not found!`)
+    }
+    return await this._promiseRequest(payloadObj.raw_url)
   }
 
-  async _set(id, value) {
-    const {
-      [HBP_DATAPROXY_WRITEURL]: writeUrl
-    } = this.keys
-
-    if (!_checkValid(writeUrl)) {
-      log({
-        breakpoint: '_set',
-        message: 'write url not valid, getting new one'
-      })
-      const url = await this._getTmpUrl('write')
-      const payload = {
-        keys: {
-          [HBP_DATAPROXY_WRITEURL]: url
+  async set(id, value) {
+    return await this._request({
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json'
+      },
+      opt: {
+        json: {
+          title: `${titlePrefix}${id}`,
+          description: `Created programmatically. v${saneUrlVer}`,
+          visibility: 'public',
+          files: [{
+            file_path: 'payload',
+            content: value
+          }]
         }
       }
-      this.redisClient.publish(HBP_OIDC_V2_UPDATE_CHAN, JSON.stringify(payload))
-      this.redisUtil.asyncSet(HBP_DATAPROXY_WRITEURL, url)
-      this.keys[HBP_DATAPROXY_WRITEURL] = url
-      log({
-        breakpoint: '_set',
-        message: 'got new write url'
-      })
-    }
-
-    await new Promise((rs, rj) => {
-      const payload = {
-        method: 'PUT',
-        uri: this._getWriteTmplTag`${DATA_PROXY_URL}/buckets/${DATA_PROXY_BUCKETNAME}/${encodeURIComponent(id)}`,
-        headers: {
-          'content-type': 'text/plain; charset=utf-8'
-        },
-        body: value
-      }
-      log({
-        breakpoint: 'pre set',
-        payload
-      })
-      request(payload, (err, resp, body) => {
-        if (err) return rj(err)
-        if (resp.statusCode === 404) return rj(new NotFoundError())
-        if (resp.statusCode >= 400) return rj(new Error(`${resp.statusCode}: ${resp.statusMessage}`))
-        if (resp.statusCode >= 200 && resp.statusCode < 300) return rs(body)
-        return rj(new Error(`[saneurl] [get] unknown error`))
-      })
     })
   }
 
-  async set(id, value) {
-    const result = await this._set(id, value)
-    return result
+  async del(id) {
+    return await this._request({
+      addPath: `/${id}`,
+      method: 'DELETE',
+    })
   }
 
-  dispose(){
-    clearInterval(this.intervalRef)
-    this.redisClient && this.redisClient.quit()
+  async dispose(){
+    
   }
 
   async healthCheck(){
-    return this.healthFlag
+    return true
   }
 }
 
-exports.FallbackStore = FallbackStore
-exports.Store = Store
+exports.GitlabSnippetStore = GitlabSnippetStore
 exports.NotFoundError = NotFoundError
diff --git a/deploy/saneUrl/util.js b/deploy/saneUrl/util.js
new file mode 100644
index 0000000000000000000000000000000000000000..949025cdc76bbf6b8794222bc67e2995ae1da482
--- /dev/null
+++ b/deploy/saneUrl/util.js
@@ -0,0 +1,68 @@
+const { NotFoundError } = require('./store')
+
+class ProxyStore {
+  static async StaticGet(store, req, name) {
+    const payload = JSON.parse(await store.get(name))
+    const { expiry, value, ...rest } = payload
+    if (expiry && (Date.now() > expiry)) {
+      await store.del(name)
+      throw new NotFoundError('Expired')
+    }
+    // backwards compatibility for depcObjStore .
+    // when depcObjStore is fully removed, || rest can also be removed
+    return value || rest
+  }
+
+  constructor(store){
+    this.store = store
+  }
+  async get(req, name) {
+    return await ProxyStore.StaticGet(this.store, req, name)
+  }
+
+  async set(req, name, value) {
+    const supplementary = req.user
+    ? {
+        userId: req.user.id,
+        expiry: null
+      }
+    : {
+        userId: null,
+        expiry: Date.now() + 1000 * 60 * 60 * 72
+      }
+    
+    const fullPayload = {
+      value,
+      ...supplementary
+    }
+    return await this.store.set(name, JSON.stringify(fullPayload))
+  }
+}
+
+const NotExactlyPromiseAny = async arg => {
+  const errs = []
+  let resolvedFlag = false
+  return await new Promise((rs, rj) => {
+    let totalCounter = 0
+    for (const pr of arg) {
+      totalCounter ++
+      pr.then(val => {
+        if (!resolvedFlag) {
+          resolvedFlag = true
+          rs(val)
+        }
+      }).catch(e => {
+        errs.push(e)
+        totalCounter --
+        if (totalCounter <= 0) {
+          rj(new NotFoundError(errs))
+        }
+      })
+    }
+  })
+}
+
+module.exports = {
+  ProxyStore,
+  NotExactlyPromiseAny
+}
diff --git a/deploy/saneUrl/util.spec.js b/deploy/saneUrl/util.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f19d8cc92e23f79d42384389a6232c6e0a1d6443
--- /dev/null
+++ b/deploy/saneUrl/util.spec.js
@@ -0,0 +1,206 @@
+const { ProxyStore, NotExactlyPromiseAny } = require('./util')
+const { expect, assert } = require('chai')
+const sinon = require('sinon')
+const { NotFoundError } = require('./store')
+
+const _name = 'foo-bar'
+const _stringValue = 'hello world'
+const _objValue = {
+  'foo': 'bar'
+}
+const _req = {}
+
+describe('> saneUrl/util.js', () => {
+  describe('> ProxyStore', () => {
+    let store = {
+      set: sinon.stub(),
+      get: sinon.stub(),
+      del: sinon.stub(),
+    }
+    beforeEach(() => {
+      store.set.returns(Promise.resolve())
+      store.get.returns(Promise.resolve('{}'))
+      store.del.returns(Promise.resolve())
+    })
+    afterEach(() => {
+      store.set.resetHistory()
+      store.get.resetHistory()
+      store.del.resetHistory()
+    })
+    describe('> StaticGet', () => {
+      it('> should call store.get', () => {
+        ProxyStore.StaticGet(store, _req, _name)
+        assert(store.get.called, 'called')
+        assert(store.get.calledWith(_name), 'called with right param')
+      })
+      describe('> if not hit', () => {
+        const err = new NotFoundError('not found')
+        beforeEach(() => {
+          store.get.rejects(err)
+        })
+        it('should throw same error', async () => {
+          try{
+            await ProxyStore.StaticGet(store, _req, _name)
+            assert(false, 'Should throw')
+          } catch (e) {
+            assert(e instanceof NotFoundError)
+          }
+        })
+      })
+      describe('> if hit', () => {
+        describe('> if expired', () => {
+          beforeEach(() => {
+            store.get.returns(
+              Promise.resolve(
+                JSON.stringify({
+                  expiry: Date.now() - 1000
+                })
+              )
+            )
+          })
+          it('> should call store.del', async () => {
+            try {
+              await ProxyStore.StaticGet(store, _req, _name)
+            } catch (e) {
+
+            }
+            assert(store.del.called, 'store.del should be called')
+          })
+          it('> should throw NotFoundError', async () => {
+            try {
+              await ProxyStore.StaticGet(store, _req, _name)
+              assert(false, 'expect throw')
+            } catch (e) {
+              assert(e instanceof NotFoundError, 'throws NotFoundError')
+            }
+          })
+        })
+        describe('> if not expired', () => {
+          it('> should return .value, if exists', async () => {
+            store.get.returns(
+              Promise.resolve(
+                JSON.stringify({
+                  value: _objValue,
+                  others: _stringValue
+                })
+              )
+            )
+            const returnObj = await ProxyStore.StaticGet(store, _req, _name)
+            expect(returnObj).to.deep.equal(_objValue)
+          })
+          it('> should return ...rest if .value does not exist', async () => {
+            store.get.returns(
+              Promise.resolve(
+                JSON.stringify({
+                  others: _stringValue
+                })
+              )
+            )
+            const returnObj = await ProxyStore.StaticGet(store, _req, _name)
+            expect(returnObj).to.deep.equal({
+              others: _stringValue
+            })
+          })
+        })
+      })
+
+    })
+
+    describe('> get', () => {
+      let staticGetStub
+      beforeEach(() => {
+        staticGetStub = sinon.stub(ProxyStore, 'StaticGet')
+        staticGetStub.returns(Promise.resolve())
+      })
+      afterEach(() => {
+        staticGetStub.restore()
+      })
+      it('> proxies calls to StaticGet', async () => {
+        const store = {}
+        const proxyStore = new ProxyStore(store)
+        await proxyStore.get(_req, _name)
+        assert(staticGetStub.called)
+        assert(staticGetStub.calledWith(store, _req, _name))
+      })
+    })
+
+    describe('> set', () => {
+      let proxyStore
+      beforeEach(() => {
+        proxyStore = new ProxyStore(store)
+        store.set.returns(Promise.resolve())
+      })
+      
+      describe('> no user', () => {
+        it('> sets expiry some time in the future', async () => {
+          await proxyStore.set(_req, _name, _objValue)
+          assert(store.set.called)
+
+          const [ name, stringifiedJson ] = store.set.args[0]
+          assert(name === _name, 'name is correct')
+          const payload = JSON.parse(stringifiedJson)
+          expect(payload.value).to.deep.equal(_objValue, 'payload is correct')
+          assert(!!payload.expiry, 'expiry exists')
+          assert((payload.expiry - Date.now()) > 1000 * 60 * 60 * 24, 'expiry is at least 24 hrs in the future')
+
+          assert(!payload.userId, 'userId does not exist')
+        })
+      })
+      describe('> yes user', () => {
+        let __req = {
+          ..._req,
+          user: {
+            id: 'foo-bar-2'
+          }
+        }
+        it('> does not set expiry, but sets userId', async () => {
+          await proxyStore.set(__req, _name, _objValue)
+          assert(store.set.called)
+
+          const [ name, stringifiedJson ] = store.set.args[0]
+          assert(name === _name, 'name is correct')
+          const payload = JSON.parse(stringifiedJson)
+          expect(payload.value).to.deep.equal(_objValue, 'payload is correct')
+          assert(!payload.expiry, 'expiry does not exist')
+
+          assert(payload.userId, 'userId exists')
+          assert(payload.userId === 'foo-bar-2', 'user-id matches')
+        })
+      })
+    })
+  })
+
+  describe('> NotExactlyPromiseAny', () => {
+    describe('> nothing resolves', () => {
+      it('> throws not found error', async () => {
+        try {
+          await NotExactlyPromiseAny([
+            (async () => {
+              throw new Error(`not here`)
+            })(),
+            new Promise((rs, rj) => setTimeout(rj, 100)),
+            new Promise((rs, rj) => rj('uhoh'))
+          ])
+          assert(false, 'expected to throw')
+        } catch (e) {
+          assert(e instanceof NotFoundError, 'expect to throw not found error')
+        }
+      })
+    })
+    describe('> something resolves', () => {
+      it('> returns the first to resolve', async () => {
+        try {
+
+          const result = await NotExactlyPromiseAny([
+            new Promise((rs, rj) => rj('uhoh')),
+            new Promise(rs => setTimeout(() => rs('hello world'), 100)),
+            Promise.resolve('foo-bar')
+          ])
+          assert(result == 'foo-bar', 'expecting first to resolve')
+        } catch (e) {
+          assert(false, 'not expecting to throw')
+        }
+      })
+    })
+  })
+})
diff --git a/docs/releases/v2.5.7.md b/docs/releases/v2.5.7.md
index 4a8698429cde82cf60e500e7cdb290a3c90a85de..f043d78016f86f7dc85fe95aea053beb348320e2 100644
--- a/docs/releases/v2.5.7.md
+++ b/docs/releases/v2.5.7.md
@@ -10,6 +10,7 @@
 ## Feature
 
 - Add menu to change perspective orientation by coronal/sagittal/axial views.
+- re-introduced saneUrl
 
 ## Under the hood
 
diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts
index 07eae088ce2154228ecc4df9f2f3779ed3390d5a..bad568f3822e640174da67c0162142b3a2188894 100644
--- a/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts
+++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.component.ts
@@ -1,16 +1,18 @@
-import { Component, Optional, ViewChild } from "@angular/core";
+import { Component, Optional, TemplateRef, ViewChild } from "@angular/core";
 import { ARIA_LABELS, CONST } from "common/constants";
 import { ModularUserAnnotationToolService } from "../tools/service";
 import { IAnnotationGeometry, TExportFormats } from "../tools/type";
 import { ComponentStore } from "src/viewerModule/componentStore";
 import { map, shareReplay, startWith } from "rxjs/operators";
-import { Observable, Subscription } from "rxjs";
+import { combineLatest, Observable, Subscription } from "rxjs";
 import { TZipFileConfig } from "src/zipFilesOutput/type";
 import { TFileInputEvent } from "src/getFileInput/type";
 import { FileInputDirective } from "src/getFileInput/getFileInput.directive";
 import { MatSnackBar } from "@angular/material/snack-bar";
 import { unzip } from "src/zipFilesOutput/zipFilesOutput.directive";
 import { DialogService } from "src/services/dialogService.service";
+import { MatDialog } from "@angular/material/dialog";
+import { userAnnotationRouteKey } from "../constants";
 
 const README = `{id}.sands.json file contains the data of annotations. {id}.desc.json contains the metadata of annotations.`
 
@@ -25,6 +27,8 @@ const README = `{id}.sands.json file contains the data of annotations. {id}.desc
 })
 export class AnnotationList {
 
+  public userAnnRoute = {}
+
   public ARIA_LABELS = ARIA_LABELS
 
   @ViewChild(FileInputDirective)
@@ -66,6 +70,7 @@ export class AnnotationList {
   constructor(
     private annotSvc: ModularUserAnnotationToolService,
     private snackbar: MatSnackBar,
+    private dialog: MatDialog,
     cStore: ComponentStore<{ useFormat: TExportFormats }>,
     @Optional() private dialogSvc: DialogService,
   ) {
@@ -74,7 +79,22 @@ export class AnnotationList {
     })
 
     this.subs.push(
-      this.managedAnnotations$.subscribe(anns => this.managedAnnotations = anns)
+      this.managedAnnotations$.subscribe(anns => this.managedAnnotations = anns),
+      combineLatest([
+        this.managedAnnotations$.pipe(
+          startWith([])
+        ),
+        this.annotationInOtherSpaces$.pipe(
+          startWith([])
+        )
+      ]).subscribe(([ann, annOther]) => {
+        this.userAnnRoute = {
+          [userAnnotationRouteKey]: [
+            ...ann.map(a => a.toJSON()),
+            ...annOther.map(a => a.toJSON()),
+          ]
+        }
+      })
     )
   }
 
@@ -175,4 +195,8 @@ export class AnnotationList {
       }
     }
   }
+
+  public openDialog(template: TemplateRef<any>) {
+    this.dialog.open(template)
+  }
 }
diff --git a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html
index 7543d5d598ce9d77956aa6036636a1ba7b8408c5..f0832efe03d55a7295ad5699d058d35d029304de 100644
--- a/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html
+++ b/src/atlasComponents/userAnnotations/annotationList/annotationList.template.html
@@ -34,6 +34,15 @@
                 <i class="fas fa-download"></i>
             </button>
 
+            <!-- share -->
+            <button mat-icon-button
+                [matTooltip]="ARIA_LABELS.SHARE_CUSTOM_URL"
+                (click)="openDialog(saneUrlTmpl)"
+                iav-state-aggregator
+                #stateAggregator="iavStateAggregator">
+                <i class="fas fa-share-square"></i>
+            </button>
+
             <!-- delete all annotations -->
             <button mat-icon-button
                 color="warn"
@@ -102,3 +111,23 @@
         </ng-template>
     </mat-card-content>
 </mat-card>
+
+
+<!-- sane url template -->
+<ng-template #saneUrlTmpl>
+    <h2 mat-dialog-title>
+        Create custom URL
+    </h2>
+    <div mat-dialog-content>
+        <iav-sane-url [stateTobeSaved]="stateAggregator.jsonifiedState$ | async | mergeObj : userAnnRoute">
+        </iav-sane-url>
+    </div>
+
+    <div mat-dialog-actions
+        class="d-flex justify-content-center">
+        <button mat-button
+            mat-dialog-close>
+            close
+        </button>
+    </div>
+</ng-template>
diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts
index b316ac2632dd0c6b31cf43bb30345434d7f37941..5eacf72e95c4baadee42cbb70d28a09ddb84dc62 100644
--- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts
+++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.component.ts
@@ -1,7 +1,5 @@
 import { Component, Inject, OnDestroy, Optional } from "@angular/core";
-import { Store } from "@ngrx/store";
 import { ModularUserAnnotationToolService } from "../tools/service";
-import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper";
 import { ARIA_LABELS } from 'common/constants'
 import { ClickInterceptor, CLICK_INTERCEPTOR_INJECTOR, CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util";
 import { TContextArg } from "src/viewerModule/viewer.interface";
@@ -31,7 +29,6 @@ export class AnnotationMode implements OnDestroy{
   private onDestroyCb: (() => void)[] = []
 
   constructor(
-    private store$: Store<any>,
     private modularToolSvc: ModularUserAnnotationToolService,
     snackbar: MatSnackBar,
     @Optional() @Inject(CLICK_INTERCEPTOR_INJECTOR) clickInterceptor: ClickInterceptor,
@@ -59,13 +56,6 @@ export class AnnotationMode implements OnDestroy{
       })
   }
 
-  exitAnnotationMode(){
-    this.store$.dispatch(
-      viewerStateSetViewerMode({
-        payload: null
-      })
-    )
-  }
   deselectTools(){
     this.modularToolSvc.deselectTools()
   }
diff --git a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html
index a21e682b08bc6b9e7adcab3737e1261fe63d9e5a..7542cdba29fd4102f5e03c801468cbc69fb77b05 100644
--- a/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html
+++ b/src/atlasComponents/userAnnotations/annotationMode/annotationMode.template.html
@@ -16,7 +16,8 @@
 
     <button
         mat-button
-        (click)="exitAnnotationMode()"
+        annotation-switch
+        annotation-switch-mode="off"
         class="tab-toggle"
         [matTooltip]="ARIA_LABELS.EXIT_ANNOTATION_MODE"
         color="warn">
diff --git a/src/atlasComponents/userAnnotations/constants.ts b/src/atlasComponents/userAnnotations/constants.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ddb14d680ab6240706d014ac1e71ebcf117d65cc
--- /dev/null
+++ b/src/atlasComponents/userAnnotations/constants.ts
@@ -0,0 +1 @@
+export const userAnnotationRouteKey = 'x-user-anntn'
diff --git a/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts
index 1109fbdbc342124d842fe82b59ee02c2a51d0de4..068f6313632fe3e79f8bbb47b2cf58be31666bac 100644
--- a/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts
+++ b/src/atlasComponents/userAnnotations/directives/annotationSwitch.directive.ts
@@ -1,13 +1,5 @@
-import { Directive, HostListener, Inject, Input, Optional } from "@angular/core";
-import { viewerStateSetViewerMode } from "src/services/state/viewerState/actions";
-import { ARIA_LABELS } from "common/constants";
-import { select, Store } from "@ngrx/store";
-import { TContextArg } from "src/viewerModule/viewer.interface";
-import { TContextMenuReg } from "src/contextMenuModule";
-import { CONTEXT_MENU_ITEM_INJECTOR, TContextMenu } from "src/util";
+import { Directive, HostListener, Input } from "@angular/core";
 import { ModularUserAnnotationToolService } from "../tools/service";
-import { Subscription } from "rxjs";
-import { viewerStateViewerModeSelector } from "src/services/state/viewerState/selectors";
 
 @Directive({
   selector: '[annotation-switch]'
@@ -17,37 +9,13 @@ export class AnnotationSwitch {
   @Input('annotation-switch-mode')
   mode: 'toggle' | 'off' | 'on' = 'on'
 
-  private currMode = null
-  private subs: Subscription[] = []
   constructor(
-    private store$: Store<any>,
     private svc: ModularUserAnnotationToolService,
-    @Optional() @Inject(CONTEXT_MENU_ITEM_INJECTOR) ctxMenuInterceptor: TContextMenu<TContextMenuReg<TContextArg<'nehuba' | 'threeSurfer'>>>
   ) {
-    this.subs.push(
-      this.store$.pipe(
-        select(viewerStateViewerModeSelector)
-      ).subscribe(mode => {
-        this.currMode = mode
-      })
-    )
   }
 
   @HostListener('click')
   onClick() {
-    let payload = null
-    if (this.mode === 'on') payload = ARIA_LABELS.VIEWER_MODE_ANNOTATING
-    if (this.mode === 'off') {
-      if (this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) payload = null
-      else return
-    }
-    if (this.mode === 'toggle') {
-      payload = this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING
-        ? null
-        : ARIA_LABELS.VIEWER_MODE_ANNOTATING
-    }
-    this.store$.dispatch(
-      viewerStateSetViewerMode({ payload })
-    )
+    this.svc.switchAnnotationMode(this.mode)
   }
 }
diff --git a/src/atlasComponents/userAnnotations/module.ts b/src/atlasComponents/userAnnotations/module.ts
index 39e7f420136db395d43f95380450eb23be2cf759..ec2586e5741ddc2d25ff4c45657154d882386946 100644
--- a/src/atlasComponents/userAnnotations/module.ts
+++ b/src/atlasComponents/userAnnotations/module.ts
@@ -1,4 +1,4 @@
-import { NgModule } from "@angular/core";
+import { APP_INITIALIZER, NgModule } from "@angular/core";
 import { CommonModule } from "@angular/common";
 import { AngularMaterialModule } from "src/sharedModules";
 import { FormsModule, ReactiveFormsModule } from "@angular/forms";
@@ -14,6 +14,9 @@ import { FileInputModule } from "src/getFileInput/module";
 import { ZipFilesOutputModule } from "src/zipFilesOutput/module";
 import { FilterAnnotationsBySpace } from "./filterAnnotationBySpace.pipe";
 import { AnnotationEventDirective } from "./directives/annotationEv.directive";
+import { ShareModule } from "src/share";
+import { StateModule } from "src/state";
+import { RoutedAnnotationService } from "./routedAnnotation.service";
 
 @NgModule({
   imports: [
@@ -26,6 +29,8 @@ import { AnnotationEventDirective } from "./directives/annotationEv.directive";
     UtilModule,
     FileInputModule,
     ZipFilesOutputModule,
+    ShareModule,
+    StateModule,
   ],
   declarations: [
     AnnotationMode,
@@ -43,6 +48,18 @@ import { AnnotationEventDirective } from "./directives/annotationEv.directive";
     AnnotationList,
     AnnotationSwitch,
     AnnotationEventDirective
+  ],
+  providers: [
+    // initialize routerannotation service, so it will parse route and load annotations ...
+    // ... in url correctly
+    {
+      provide: APP_INITIALIZER,
+      useFactory:(svc: RoutedAnnotationService) => {
+        return () => Promise.resolve()
+      },
+      deps: [ RoutedAnnotationService ],
+      multi: true
+    }
   ]
 })
 
diff --git a/src/atlasComponents/userAnnotations/routedAnnotation.service.spec.ts b/src/atlasComponents/userAnnotations/routedAnnotation.service.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7aecdd3d4a043e42699d2a8d2279c568bf48191c
--- /dev/null
+++ b/src/atlasComponents/userAnnotations/routedAnnotation.service.spec.ts
@@ -0,0 +1,178 @@
+import { fakeAsync, TestBed, tick } from "@angular/core/testing"
+import { BehaviorSubject, of } from "rxjs"
+import { RouterService } from "src/routerModule/router.service"
+import { SaneUrlSvc } from "src/share/saneUrl/saneUrl.service"
+import { ModularUserAnnotationToolService } from "./tools/service"
+import { RoutedAnnotationService } from './routedAnnotation.service'
+import { userAnnotationRouteKey } from "./constants"
+
+describe('> routedannotation.service.ts', () => {
+  describe('> RoutedAnnotationService', () => {
+    const customRouteSub = new BehaviorSubject(null)
+    const spyRService = {
+      customRoute$: customRouteSub.asObservable()
+    }
+    
+    const spyAnnSvc = {
+      switchAnnotationMode: jasmine.createSpy('switchAnnotationMode'),
+      parseAnnotationObject: jasmine.createSpy('parseAnnotationObject'),
+      importAnnotation: jasmine.createSpy('importAnnotation')
+    }
+    beforeEach(() => {
+      TestBed.configureTestingModule({
+        providers: [
+          RoutedAnnotationService,
+          {
+            provide: RouterService,
+            useValue: spyRService
+          }, {
+            provide: SaneUrlSvc,
+            useFactory: () => {
+              return {
+                getKeyVal: jasmine.createSpy('getKeyVal').and.returnValue(
+                  of({})
+                )
+              }
+            }
+          }, {
+            provide: ModularUserAnnotationToolService,
+            useValue: spyAnnSvc
+          }
+        ],
+      })
+    })
+    afterEach(() => {
+      spyAnnSvc.switchAnnotationMode.calls.reset()
+      spyAnnSvc.parseAnnotationObject.calls.reset()
+      spyAnnSvc.importAnnotation.calls.reset()
+    })
+
+    it('> can be init', () => {
+      const svc = TestBed.inject(RoutedAnnotationService)
+      expect(svc instanceof RoutedAnnotationService).toBeTrue()
+    })
+
+    describe('> normal operation', () => {
+      const mockVal = 'foo-bar'
+      const getKeyValReturn = {
+        [userAnnotationRouteKey]: [{
+          foo: 'bar'
+        }, {
+          foo: 'hello world'
+        }]
+      }
+      const parseAnnObjReturn = [{
+        bar: 'baz'
+      }, {
+        hello: 'world'
+      }]
+      
+      beforeEach(() => {
+        const spySaneUrlSvc = TestBed.inject(SaneUrlSvc) as any
+        spySaneUrlSvc.getKeyVal.and.returnValue(
+          of(getKeyValReturn)
+        )
+        spyAnnSvc.parseAnnotationObject.and.returnValues(...parseAnnObjReturn)
+      })
+
+      it('> getKeyVal called after at least 160ms', fakeAsync(() => {
+        customRouteSub.next({
+          [userAnnotationRouteKey]: mockVal
+        })
+        const svc = TestBed.inject(RoutedAnnotationService)
+        tick(200)
+        const spySaneUrlSvc = TestBed.inject(SaneUrlSvc) as any
+        expect(spySaneUrlSvc.getKeyVal).toHaveBeenCalled()
+        expect(spySaneUrlSvc.getKeyVal).toHaveBeenCalledWith(mockVal)
+      }))
+
+      it('> switchannotation mode is called with "on"', fakeAsync(() => {
+        customRouteSub.next({
+          [userAnnotationRouteKey]: mockVal
+        })
+        const svc = TestBed.inject(RoutedAnnotationService)
+        tick(200)
+        expect(spyAnnSvc.switchAnnotationMode).toHaveBeenCalledOnceWith('on')
+      }))
+
+      it('> parseAnnotationObject is called expected number of times', fakeAsync(() => {
+        customRouteSub.next({
+          [userAnnotationRouteKey]: mockVal
+        })
+        const svc = TestBed.inject(RoutedAnnotationService)
+        tick(200)
+
+        const userAnn = getKeyValReturn[userAnnotationRouteKey]
+        expect(spyAnnSvc.parseAnnotationObject).toHaveBeenCalledTimes(userAnn.length)
+        for (const ann of userAnn) {
+          expect(spyAnnSvc.parseAnnotationObject).toHaveBeenCalledWith(ann)
+        }
+      }))
+
+      it('> importAnnotation is called expected number of times', fakeAsync(() => {
+        customRouteSub.next({
+          [userAnnotationRouteKey]: mockVal
+        })
+        const svc = TestBed.inject(RoutedAnnotationService)
+        tick(200)
+        expect(spyAnnSvc.importAnnotation).toHaveBeenCalledTimes(parseAnnObjReturn.length)
+        for (const obj of parseAnnObjReturn) {
+          expect(spyAnnSvc.importAnnotation).toHaveBeenCalledWith(obj)
+        }
+      }))
+    })
+
+    describe('> abnormal operation', () => {
+      describe('> routerSvc.customRoute$ emits after 160 ms', () => {
+        const mockVal = 'foo-bar'
+
+        it('> getKeyVal should only be called once', fakeAsync(() => {
+          customRouteSub.next({
+            [userAnnotationRouteKey]: mockVal
+          })
+          const svc = TestBed.inject(RoutedAnnotationService)
+          tick(200)
+          customRouteSub.next({
+            [userAnnotationRouteKey]: 'hello world'
+          })
+          tick(200)
+          const spySaneUrlSvc = TestBed.inject(SaneUrlSvc) as any
+          expect(spySaneUrlSvc.getKeyVal).toHaveBeenCalledOnceWith(mockVal)
+        }))
+      })
+
+      describe('> routerSvc.customRoute$ does not emit valid key', () => {
+        it('> does not call getKeyVal', fakeAsync(() => {
+          customRouteSub.next({
+            'hello-world': 'foo-bar'
+          })
+          const svc = TestBed.inject(RoutedAnnotationService)
+          tick(200)
+          const spySaneUrlSvc = TestBed.inject(SaneUrlSvc) as any
+          expect(spySaneUrlSvc.getKeyVal).not.toHaveBeenCalled()
+        }))
+      })
+
+      describe('> getKeyVal returns invalid key', () => {
+        it('> does not call switchAnnotationMode', fakeAsync(() => {
+          const spySaneUrlSvc = TestBed.inject(SaneUrlSvc) as any
+          spySaneUrlSvc.getKeyVal.and.returnValue(
+            of({
+              'hello-world': [{
+                foo: 'bar',
+                fuzz: 'bizz'
+              }]
+            })
+          )
+          customRouteSub.next({
+            [userAnnotationRouteKey]: 'foo-bar'
+          })
+          const svc = TestBed.inject(RoutedAnnotationService)
+
+          tick(320)
+          expect(spyAnnSvc.switchAnnotationMode).not.toHaveBeenCalled()
+        }))
+      })
+    })
+  })
+})
diff --git a/src/atlasComponents/userAnnotations/routedAnnotation.service.ts b/src/atlasComponents/userAnnotations/routedAnnotation.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..11a28a8fe6548ede886ab3b0c0ca1f09125aa918
--- /dev/null
+++ b/src/atlasComponents/userAnnotations/routedAnnotation.service.ts
@@ -0,0 +1,41 @@
+import { Injectable } from "@angular/core";
+import { NEVER } from "rxjs";
+import { debounceTime, map, switchMap, take } from "rxjs/operators";
+import { RouterService } from "src/routerModule/router.service";
+import { SaneUrlSvc } from "src/share/saneUrl/saneUrl.service";
+import { userAnnotationRouteKey } from "./constants";
+import { ModularUserAnnotationToolService } from "./tools/service";
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class RoutedAnnotationService{
+  constructor(
+    routerSvc: RouterService,
+    saneUrlSvc: SaneUrlSvc,
+    annSvc: ModularUserAnnotationToolService,
+  ){
+
+    routerSvc.customRoute$.pipe(
+      debounceTime(160),
+      take(1),
+      map(obj => obj[userAnnotationRouteKey]),
+      switchMap(
+        saneUrlKey => {
+          return saneUrlKey
+            ? saneUrlSvc.getKeyVal(saneUrlKey)
+            : NEVER
+        }
+      )
+    ).subscribe(val => {
+      if (val[userAnnotationRouteKey]) {
+        annSvc.switchAnnotationMode('on')
+        for (const ann of val[userAnnotationRouteKey]){
+          const geom = annSvc.parseAnnotationObject(ann)
+          annSvc.importAnnotation(geom)
+        }
+      }
+    })
+  }
+}
diff --git a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts
index 956f23c080c1929b07fc62159a42024c68334490..8c9a11566b9f7f27350a684cc9ffc3976525c32e 100644
--- a/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts
+++ b/src/atlasComponents/userAnnotations/singleAnnotationUnit/singleAnnotationUnit.component.ts
@@ -48,8 +48,8 @@ export class SingleAnnotationUnit implements OnDestroy, AfterViewInit{
     this.chSubs.push(
       this.formGrp.valueChanges.subscribe(value => {
         const { name, desc, spaceId } = value
-        this.managedAnnotation.setName(name)
-        this.managedAnnotation.setDesc(desc)
+        this.managedAnnotation.name = name
+        this.managedAnnotation.desc = desc
       })
     )
 
diff --git a/src/atlasComponents/userAnnotations/tools/line.ts b/src/atlasComponents/userAnnotations/tools/line.ts
index 606c155f3e995bbf3bba85d6b82cb4040ae7a76e..bea53cdf42b49e494e9f3419813f4b9178dc3b99 100644
--- a/src/atlasComponents/userAnnotations/tools/line.ts
+++ b/src/atlasComponents/userAnnotations/tools/line.ts
@@ -45,7 +45,7 @@ export class Line extends IAnnotationGeometry{
       })
     if (!this.points[0]) this.points[0] = point
     else this.points[1] = point
-    this.sendUpdateSignal()
+    this.changed()
     return point
   }
 
@@ -172,11 +172,7 @@ export class Line extends IAnnotationGeometry{
     for (const p of this.points){
       p.translate(x, y, z)
     }
-    this.sendUpdateSignal()
-  }
-
-  private sendUpdateSignal(){
-    this.updateSignal$.next(this.toString())
+    this.changed()
   }
 }
 
@@ -244,7 +240,7 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On
          * it only has a single point, and should be removed
          */
         if (this.selectedLine) {
-          this.removeAnnotation(this.selectedLine.id)
+          this.removeAnnotation(this.selectedLine)
         }
         this.selectedLine = null
       }),
@@ -311,14 +307,4 @@ export class ToolLine extends AbsToolClass<Line> implements IAnnotationTools, On
   ngOnDestroy(){
     this.subs.forEach(s => s.unsubscribe())
   }
-
-  removeAnnotation(id: string){
-    const idx = this.managedAnnotations.findIndex(ann => ann.id === id)
-    if (idx < 0) {
-      return
-    }
-    this.managedAnnotations.splice(idx, 1)
-    this.managedAnnotations$.next(this.managedAnnotations)
-  }
-
 }
diff --git a/src/atlasComponents/userAnnotations/tools/point.ts b/src/atlasComponents/userAnnotations/tools/point.ts
index af1543fed9b100ecabda6373bf4055b685c580f7..1db473a6713ed78b82c0d12c4a34acf65f13bb84 100644
--- a/src/atlasComponents/userAnnotations/tools/point.ts
+++ b/src/atlasComponents/userAnnotations/tools/point.ts
@@ -95,7 +95,7 @@ export class Point extends IAnnotationGeometry {
     this.x += x
     this.y += y
     this.z += z
-    this.updateSignal$.next(this.toString())
+    this.changed()
   }
 }
 
@@ -177,20 +177,6 @@ export class ToolPoint extends AbsToolClass<Point> implements IAnnotationTools,
     )
   }
 
-  /**
-   * @description remove managed annotation via id
-   * @param id id of annotation
-   */
-  removeAnnotation(id: string) {
-    const idx = this.managedAnnotations.findIndex(ann => id === ann.id)
-    if (idx < 0){
-      throw new Error(`cannot find point idx ${idx}`)
-      return
-    }
-    this.managedAnnotations.splice(idx, 1)
-    this.managedAnnotations$.next(this.managedAnnotations)
-  }
-
   onMouseMoveRenderPreview(pos: [number, number, number]) {
     return [{
       id: `${ToolPoint.PREVIEW_ID}_0`,
diff --git a/src/atlasComponents/userAnnotations/tools/poly.ts b/src/atlasComponents/userAnnotations/tools/poly.ts
index 2cd9285b256119f8ec112825b96fc615a6eea407..b7929b5dfe77e378ae6a52c2f5c67d5333547025 100644
--- a/src/atlasComponents/userAnnotations/tools/poly.ts
+++ b/src/atlasComponents/userAnnotations/tools/poly.ts
@@ -37,7 +37,7 @@ export class Polygon extends IAnnotationGeometry{
     this.edges = this.edges.filter(([ idx1, idx2 ]) => idx1 !== ptIdx && idx2 !== ptIdx)
     this.points.splice(ptIdx, 1)
 
-    this.sendUpdateSignal()
+    this.changed()
   }
 
   public addPoint(p: Point | {x: number, y: number, z: number}, linkTo?: Point): Point {
@@ -56,7 +56,7 @@ export class Polygon extends IAnnotationGeometry{
     if (!this.hasPoint(pointToBeAdded)) {
       this.points.push(pointToBeAdded)
       const sub = pointToBeAdded.updateSignal$.subscribe(
-        () => this.sendUpdateSignal()
+        () => this.changed()
       )
       this.ptWkMp.set(pointToBeAdded, {
         onremove: () => {
@@ -71,7 +71,7 @@ export class Polygon extends IAnnotationGeometry{
       ] as [number, number]
       this.edges.push(newEdge)
     }
-    this.sendUpdateSignal()
+    this.changed()
     return pointToBeAdded
   }
 
@@ -209,15 +209,11 @@ export class Polygon extends IAnnotationGeometry{
     this.edges = edges
   }
 
-  private sendUpdateSignal(){
-    this.updateSignal$.next(this.toString())
-  }
-
   public translate(x: number, y: number, z: number) {
     for (const p of this.points){
       p.translate(x, y, z)
     }
-    this.sendUpdateSignal()
+    this.changed()
   }
 }
 
@@ -307,7 +303,7 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo
            * if edges < 3, discard poly
            */
           if (edges.length < 3) {
-            this.removeAnnotation(this.selectedPoly.id)
+            this.removeAnnotation(this.selectedPoly)
           }
         }
 
@@ -404,15 +400,6 @@ export class ToolPolygon extends AbsToolClass<Polygon> implements IAnnotationToo
     )
   }
 
-  removeAnnotation(id: string) {
-    const idx = this.managedAnnotations.findIndex(ann => ann.id === id)
-    if (idx < 0) {
-      return
-    }
-    this.managedAnnotations.splice(idx, 1)
-    this.managedAnnotations$.next(this.managedAnnotations)
-  }
-
   ngOnDestroy(){
     if (this.subs.length > 0) this.subs.pop().unsubscribe()
   }
diff --git a/src/atlasComponents/userAnnotations/tools/service.ts b/src/atlasComponents/userAnnotations/tools/service.ts
index 8185ed4b6177cb3688ca381c87a4c7a05961ada4..196d77ec0563fa57183f10fb2c7d865e462f6d00 100644
--- a/src/atlasComponents/userAnnotations/tools/service.ts
+++ b/src/atlasComponents/userAnnotations/tools/service.ts
@@ -15,6 +15,7 @@ import { Point } from "./point";
 import { FilterAnnotationsBySpace } from "../filterAnnotationBySpace.pipe";
 import { retry } from 'common/util'
 import { MatSnackBar } from "@angular/material/snack-bar";
+import { viewerStateSetViewerMode } from "src/services/state/viewerState.store.helper";
 
 const LOCAL_STORAGE_KEY = 'userAnnotationKey'
 
@@ -490,6 +491,7 @@ export class ModularUserAnnotationToolService implements OnDestroy{
       store.pipe(
         select(viewerStateViewerModeSelector)
       ).subscribe(viewerMode => {
+        this.currMode = viewerMode
         if (viewerMode === ModularUserAnnotationToolService.VIEWER_MODE) {
           if (this.ngAnnotationLayer) this.ngAnnotationLayer.setVisible(true)
           else {
@@ -697,8 +699,8 @@ export class ModularUserAnnotationToolService implements OnDestroy{
 
         // potentially overwriting existing name and desc...
         // maybe should show warning?
-        existingAnn.setName(json.name)
-        existingAnn.setDesc(json.desc)
+        existingAnn.name = json.name
+        existingAnn.desc = json.desc
         return existingAnn
       } else {
         const { id, name, desc } = json
@@ -708,8 +710,8 @@ export class ModularUserAnnotationToolService implements OnDestroy{
     } else {
       const metadata = this.metadataMap.get(returnObj.id)
       if (returnObj && metadata) {
-        returnObj.setName(metadata?.name || null)
-        returnObj.setDesc(metadata?.desc || null)
+        returnObj.name = metadata?.name || null
+        returnObj.desc = metadata?.desc || null
         this.metadataMap.delete(returnObj.id)
       }
     }
@@ -730,6 +732,25 @@ export class ModularUserAnnotationToolService implements OnDestroy{
   ngOnDestroy(){
     while(this.subscription.length > 0) this.subscription.pop().unsubscribe()
   }
+
+  private currMode: string
+  switchAnnotationMode(mode: 'on' | 'off' | 'toggle' = 'toggle') {
+
+    let payload = null
+    if (mode === 'on') payload = ARIA_LABELS.VIEWER_MODE_ANNOTATING
+    if (mode === 'off') {
+      if (this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING) payload = null
+      else return
+    }
+    if (mode === 'toggle') {
+      payload = this.currMode === ARIA_LABELS.VIEWER_MODE_ANNOTATING
+        ? null
+        : ARIA_LABELS.VIEWER_MODE_ANNOTATING
+    }
+    this.store.dispatch(
+      viewerStateSetViewerMode({ payload })
+    )
+  }
 }
 
 export function parseNgAnnotation(ann: INgAnnotationTypes[keyof INgAnnotationTypes]){
diff --git a/src/atlasComponents/userAnnotations/tools/type.spec.ts b/src/atlasComponents/userAnnotations/tools/type.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f4b28738913465c7eda8ee898f28fdc724dbf016
--- /dev/null
+++ b/src/atlasComponents/userAnnotations/tools/type.spec.ts
@@ -0,0 +1,166 @@
+import { Subject, Subscription } from "rxjs"
+import { AbsToolClass, IAnnotationEvents, IAnnotationGeometry, TAnnotationEvent } from "./type"
+
+class TmpCls extends IAnnotationGeometry{
+  getNgAnnotationIds(){
+    return []
+  }
+  toNgAnnotation() {
+    return []
+  }
+  toJSON() {
+    return {}
+  }
+  toString() {
+    return ''
+  }
+  toSands(){
+    return {} as any
+  }
+}
+
+class TmpToolCls extends AbsToolClass<TmpCls> {
+  iconClass = ''
+  name: 'tmplClsTool'
+  subs = []
+  onMouseMoveRenderPreview() {
+    return []
+  }
+
+  protected managedAnnotations: TmpCls[] = []
+  public managedAnnotations$ = new Subject<TmpCls[]>()
+}
+
+describe('> types.ts', () => {
+  describe('> AbsToolClass', () => {
+    let tool: TmpToolCls
+    let ann: TmpCls
+    let managedAnn: jasmine.Spy
+    const subs: Subscription[] = []
+
+    beforeEach(() => {
+      const ev$ = new Subject<TAnnotationEvent<keyof IAnnotationEvents>>()
+      tool = new TmpToolCls(ev$, () => {})
+      ann = new TmpCls()
+      managedAnn = jasmine.createSpy('managedannspy')
+      subs.push(
+        tool.managedAnnotations$.subscribe(managedAnn)
+      )
+    })
+
+    afterEach(() => {
+      managedAnn.calls.reset()
+      while(subs.length) subs.pop().unsubscribe()
+    })
+
+    it('> shuld init just fine', () => {
+      expect(true).toEqual(true)
+    })
+    describe('> managedAnnotations$', () => {
+      describe('> on point add', () => {
+        it('> should emit new managedannotations', () => {
+          tool.addAnnotation(ann)
+          expect(managedAnn).toHaveBeenCalled()
+          expect(managedAnn).toHaveBeenCalledTimes(1)
+          const firstCallArgs = managedAnn.calls.allArgs()[0]
+          const firstArg = firstCallArgs[0]
+          expect(firstArg).toEqual([ann])
+        })
+      })
+      describe('> on point update', () => {
+        it('> should emit new managedannotations', () => {
+          tool.addAnnotation(ann)
+          ann.name = 'blabla'
+          expect(managedAnn).toHaveBeenCalledTimes(2)
+          const firstCallArgs = managedAnn.calls.allArgs()[0]
+          const secondCallArgs = managedAnn.calls.allArgs()[1]
+          expect(firstCallArgs).toEqual(secondCallArgs)
+        })
+      })
+      describe('> on point rm', () => {
+        it('> managed annotation === 0', () => {
+          tool.addAnnotation(ann)
+
+          const firstCallArgs = managedAnn.calls.allArgs()[0]
+          const subManagedAnn0 = firstCallArgs[0]
+          expect(subManagedAnn0.length).toEqual(1)
+
+          ann.remove()
+          expect(managedAnn).toHaveBeenCalledTimes(2)
+
+          const secondCallArgs = managedAnn.calls.allArgs()[1]
+          const subManagedAnn1 = secondCallArgs[0]
+          expect(subManagedAnn1.length).toEqual(0)
+        })
+        it('> does not trigger after rm', () => {
+          tool.addAnnotation(ann)
+          ann.remove()
+          ann.name = 'blabla'
+          expect(managedAnn).toHaveBeenCalledTimes(2)
+        })
+      })
+    })
+  })
+
+  describe('> IAnnotationGeometry', () => {
+    it('> can be init fine', () => {
+      new TmpCls()
+      expect(true).toBe(true)
+    })
+
+    describe('> updateSignal$', () => {
+      class TmpCls extends IAnnotationGeometry{
+        getNgAnnotationIds(){
+          return []
+        }
+        toNgAnnotation() {
+          return []
+        }
+        toJSON() {
+          return {}
+        }
+        toString() {
+          return `${this.name || ''}:${this.desc || ''}`
+        }
+        toSands(){
+          return {} as any
+        }
+      }
+
+      let tmp: TmpCls
+      let subs: Subscription[] = []
+      let updateStub: jasmine.Spy
+      beforeEach(() => {
+        tmp = new TmpCls()
+        updateStub = jasmine.createSpy('updateSpy')
+        subs.push(
+          tmp.updateSignal$.subscribe(
+            val => updateStub(val)
+          )
+        )
+      })
+      afterEach(() => {
+        while(subs.length) subs.pop().unsubscribe()
+        updateStub.calls.reset()
+      })
+      it('> is fired on setting name', () => {
+        tmp.name = 'test'
+        expect(updateStub).toHaveBeenCalled()
+        expect(updateStub).toHaveBeenCalledTimes(1)
+      })
+
+      it('> is fired on setting desc', () => {
+        tmp.desc = 'testdesc'
+        expect(updateStub).toHaveBeenCalled()
+        expect(updateStub).toHaveBeenCalledTimes(1)
+      })
+
+      it('> should be fired even if same string', () => {
+
+        tmp.name = null
+        tmp.name = undefined
+        expect(updateStub).toHaveBeenCalledTimes(2)
+      })
+    })
+  })
+})
diff --git a/src/atlasComponents/userAnnotations/tools/type.ts b/src/atlasComponents/userAnnotations/tools/type.ts
index e74f0ca8ee389775b93f49836a4f504ab88ccfec..85a4c2e0f6783a024017a34fc0b50588a89ccfcd 100644
--- a/src/atlasComponents/userAnnotations/tools/type.ts
+++ b/src/atlasComponents/userAnnotations/tools/type.ts
@@ -18,9 +18,8 @@ export abstract class AbsToolClass<T extends IAnnotationGeometry> {
   public abstract name: string
   public abstract iconClass: string
 
-  public abstract removeAnnotation(id: string): void
   public abstract managedAnnotations$: Subject<T[]>
-  protected abstract managedAnnotations: T[] = []
+  protected managedAnnotations: T[] = []
 
   abstract subs: Subscription[]
   protected space: TBaseAnnotationGeomtrySpec['space']
@@ -158,10 +157,26 @@ export abstract class AbsToolClass<T extends IAnnotationGeometry> {
   public addAnnotation(geom: T) {
     const found = this.managedAnnotations.find(ann => ann.id === geom.id)
     if (found) found.remove()
-    geom.remove = () => this.removeAnnotation(geom.id)
+    const sub = geom.updateSignal$.subscribe(() => {
+      this.managedAnnotations$.next(this.managedAnnotations)
+    })
+    geom.remove = () => {
+      this.removeAnnotation(geom)
+      sub.unsubscribe()
+    }
     this.managedAnnotations.push(geom)
     this.managedAnnotations$.next(this.managedAnnotations)
   }
+
+  public removeAnnotation(geom: T) {
+    const idx = this.managedAnnotations.findIndex(ann => ann.id === geom.id)
+    if (idx < 0) {
+      console.warn(`removeAnnotation error: cannot annotation with id: ${geom.id}`)
+      return
+    }
+    this.managedAnnotations.splice(idx, 1)
+    this.managedAnnotations$.next(this.managedAnnotations)
+  }
 }
 
 export type TToolType = 'selecting' | 'drawing' | 'deletion'
@@ -276,8 +291,25 @@ export abstract class Highlightable {
 export abstract class IAnnotationGeometry extends Highlightable {
   public id: string
   
-  public name: string
-  public desc: string
+  private _name: string
+  set name(val: string) {
+    if (val === this._name) return
+    this._name = val
+    this.changed()
+  }
+  get name(): string {
+    return this._name
+  }
+
+  private _desc: string
+  set desc(val: string) {
+    if (val === this._desc) return
+    this._desc = val
+    this.changed()
+  }
+  get desc(): string {
+    return this._desc
+  }
 
   public space: TBaseAnnotationGeomtrySpec['space']
 
@@ -290,7 +322,13 @@ export abstract class IAnnotationGeometry extends Highlightable {
   public remove() {
     throw new Error(`The remove method needs to be overwritten by the tool manager`)
   }
-  public updateSignal$ = new Subject()
+
+  private _updateSignal$ = new Subject()
+
+  public updateSignal$ = this._updateSignal$.asObservable()
+  protected changed(){
+    this._updateSignal$.next(Date.now())
+  }
 
   constructor(spec?: TBaseAnnotationGeomtrySpec){
     super()
@@ -299,15 +337,6 @@ export abstract class IAnnotationGeometry extends Highlightable {
     this.name = spec?.name
     this.desc = spec?.desc
   }
-
-  setName(name: string) {
-    this.name = name
-    this.updateSignal$.next(this.toString())
-  }
-  setDesc(desc: string) {
-    this.desc = desc
-    this.updateSignal$.next(this.toString())
-  }
 }
 
 export interface IAnnotationTools {
diff --git a/src/routerModule/router.service.spec.ts b/src/routerModule/router.service.spec.ts
index 5507f4a5ba5d4bb05c86853bf91768b4d7eaf213..2b933c6f1a5711b8503776d082203e684299f563 100644
--- a/src/routerModule/router.service.spec.ts
+++ b/src/routerModule/router.service.spec.ts
@@ -3,6 +3,7 @@ import { discardPeriodicTasks, fakeAsync, TestBed, tick } from "@angular/core/te
 import { Router } from "@angular/router"
 import { RouterTestingModule } from '@angular/router/testing'
 import { MockStore, provideMockStore } from "@ngrx/store/testing"
+import { cold } from "jasmine-marbles"
 import { of } from "rxjs"
 import { PureContantService } from "src/util"
 import { RouterService } from "./router.service"
@@ -290,5 +291,114 @@ describe('> router.service.ts', () => {
         })
       })
     })
+
+    describe('> customRoute$', () => {
+      let decodeCustomStateSpy: jasmine.Spy
+      beforeEach(() => {
+        decodeCustomStateSpy = jasmine.createSpy('decodeCustomState')
+        spyOnProperty(util, 'decodeCustomState').and.returnValue(decodeCustomStateSpy)
+
+        router = TestBed.inject(Router)
+      })
+
+      afterEach(() => {
+        decodeCustomStateSpy.calls.reset()
+      })
+      
+      it('> emits return record from decodeCustomState', fakeAsync(() => {
+        const value = {
+          'x-foo': 'bar'
+        }
+        decodeCustomStateSpy.and.returnValue(value)
+        const rService = TestBed.inject(RouterService)
+        router.navigate(['foo'])
+        tick(320)
+        
+        expect(rService.customRoute$).toBeObservable(
+          cold('a', {
+            a: {
+              'x-foo': 'bar'
+            }
+          })
+        )
+        discardPeriodicTasks()
+      }))
+      it('> merges observable from _customRoutes$', fakeAsync(() => {
+        decodeCustomStateSpy.and.returnValue({})
+        const rService = TestBed.inject(RouterService)
+        rService.setCustomRoute('x-fizz', 'buzz')
+        tick(320)
+        
+        expect(rService.customRoute$).toBeObservable(
+          cold('(ba)', {
+            a: {
+              'x-fizz': 'buzz'
+            },
+            b: {}
+          })
+        )
+        discardPeriodicTasks()
+      }))
+
+      it('> merges from both sources', fakeAsync(() => {
+        const value = {
+          'x-foo': 'bar'
+        }
+        decodeCustomStateSpy.and.returnValue(value)
+        const rService = TestBed.inject(RouterService)
+        rService.setCustomRoute('x-fizz', 'buzz')
+        tick(320)
+        
+        expect(rService.customRoute$).toBeObservable(
+          cold('(ba)', {
+            a: {
+              'x-fizz': 'buzz',
+              'x-foo': 'bar'
+            },
+            b: {
+              'x-foo': 'bar'
+            }
+          })
+        )
+        discardPeriodicTasks()
+      }))
+
+      it('> subsequent emits overwrites', fakeAsync(() => {
+        decodeCustomStateSpy.and.returnValue({})
+        const rService = TestBed.inject(RouterService)
+        spyOn(router, 'navigateByUrl').and.callFake((() => {
+          console.log('navigate by url')
+        }) as any)
+
+        const customRouteSpy = jasmine.createSpy('customRouteSpy')
+        rService.customRoute$.subscribe(customRouteSpy)
+        
+        rService.setCustomRoute('x-fizz', 'buzz')
+        tick(20)
+        rService.setCustomRoute('x-foo', 'bar')
+        tick(20)
+        rService.setCustomRoute('x-foo', null)
+
+        tick(320)
+
+        const expectedCalls = {
+          z: {
+            'x-fizz': 'buzz',
+            'x-foo': null
+          },
+          a: {
+            'x-fizz': 'buzz',
+            'x-foo': 'bar'
+          },
+          b: {
+            'x-fizz': 'buzz'
+          }
+        }
+        for (const c in expectedCalls) {
+          expect(customRouteSpy).toHaveBeenCalledWith(expectedCalls[c])
+        }
+        discardPeriodicTasks()
+      }))
+    })
   })
 })
diff --git a/src/routerModule/router.service.ts b/src/routerModule/router.service.ts
index 0a9ce582a948aae79538f17c67401d8c7b079257..ee0246f79302d60fae4c9caa8477c65120929d52 100644
--- a/src/routerModule/router.service.ts
+++ b/src/routerModule/router.service.ts
@@ -3,10 +3,12 @@ import { APP_BASE_HREF } from "@angular/common";
 import { Inject } from "@angular/core";
 import { NavigationEnd, Router } from '@angular/router'
 import { Store } from "@ngrx/store";
-import { debounceTime, filter, map, shareReplay, switchMapTo, take, withLatestFrom } from "rxjs/operators";
+import { debounceTime, distinctUntilChanged, filter, map, shareReplay, startWith, switchMapTo, take, tap, withLatestFrom } from "rxjs/operators";
 import { generalApplyState } from "src/services/stateStore.helper";
 import { PureContantService } from "src/util";
-import { cvtStateToHashedRoutes, cvtFullRouteToState } from "./util";
+import { cvtStateToHashedRoutes, cvtFullRouteToState, encodeCustomState, decodeCustomState, verifyCustomState } from "./util";
+import { BehaviorSubject, combineLatest, merge, Observable } from 'rxjs'
+import { scan } from 'rxjs/operators'
 
 @Injectable({
   providedIn: 'root'
@@ -18,6 +20,21 @@ export class RouterService {
     console.log(...e)
   }
 
+  private _customRoute$ = new BehaviorSubject<{
+    [key: string]: string
+  }>({})
+
+  public customRoute$: Observable<Record<string, any>>
+
+  setCustomRoute(key: string, state: string){
+    if (!verifyCustomState(key)) {
+      throw new Error(`custom state key must start with x- `)
+    }
+    this._customRoute$.next({
+      [key]: state
+    })
+  }
+
   constructor(
     router: Router,
     pureConstantService: PureContantService,
@@ -40,6 +57,40 @@ export class RouterService {
       shareReplay(1),
     )
 
+    this.customRoute$ = ready$.pipe(
+      switchMapTo(
+        merge(
+          navEnd$.pipe(
+            map((ev: NavigationEnd) => {
+              const fullPath = ev.urlAfterRedirects
+              const customState = decodeCustomState(
+                router.parseUrl(fullPath)
+              )
+              return customState || {}
+            }),
+          ),
+          this._customRoute$
+        ).pipe(
+          scan<Record<string, string>>((acc, curr) => {
+            return {
+              ...acc,
+              ...curr
+            }
+          }, {}),
+          // TODO add obj eql distinctuntilchanged check
+          distinctUntilChanged((o, n) => {
+            if (Object.keys(o).length !== Object.keys(n).length) {
+              return false
+            }
+            for (const key in o) {
+              if (o[key] !== n[key]) return false
+            }
+            return true
+          }),
+        )
+      ),
+    )
+
     ready$.pipe(
       switchMapTo(
         navEnd$.pipe(
@@ -70,15 +121,28 @@ export class RouterService {
     // which may or many not be 
     ready$.pipe(
       switchMapTo(
-        store$.pipe(
-          debounceTime(160),
-          map(state => {
-            try {
-              return cvtStateToHashedRoutes(state)
-            } catch (e) {
-              this.logError(e)
-              return ``
+        combineLatest([
+          store$.pipe(
+            debounceTime(160),
+            map(state => {
+              try {
+                return cvtStateToHashedRoutes(state)
+              } catch (e) {
+                this.logError(e)
+                return ``
+              }
+            })
+          ),
+          this.customRoute$,
+        ]).pipe(
+          map(([ routePath, customPath ]) => {
+            let returnPath = routePath
+            for (const key in customPath) {
+              const customStatePath = encodeCustomState(key, customPath[key])
+              if (!customStatePath) continue
+              returnPath += `/${customStatePath}`
             }
+            return returnPath
           })
         )
       )
diff --git a/src/routerModule/util.spec.ts b/src/routerModule/util.spec.ts
index b2ed55ea82db7f09f581114c2157a40ad2731bb6..35e0baefc23aa62bd6531b3ea32a2659744c63e7 100644
--- a/src/routerModule/util.spec.ts
+++ b/src/routerModule/util.spec.ts
@@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing'
 import { MockStore, provideMockStore } from '@ngrx/store/testing'
 import { uiStatePreviewingDatasetFilesSelector } from 'src/services/state/uiState/selectors'
 import { viewerStateGetSelectedAtlas, viewerStateSelectedParcellationSelector, viewerStateSelectedRegionsSelector, viewerStateSelectedTemplateSelector, viewerStateSelectorNavigation, viewerStateSelectorStandaloneVolumes } from 'src/services/state/viewerState/selectors'
-import { cvtFullRouteToState, cvtStateToHashedRoutes, DummyCmp, routes } from './util'
+import { cvtFullRouteToState, cvtStateToHashedRoutes, DummyCmp, encodeCustomState, routes, verifyCustomState } from './util'
 import { encodeNumber } from './cipher'
 import { Router } from '@angular/router'
 import { RouterTestingModule } from '@angular/router/testing'
@@ -158,4 +158,35 @@ describe('> util.ts', () => {
       })
     })
   })
+
+  describe('> verifyCustomState', () => {
+    it('> should return false on bad custom state', () => {
+      expect(verifyCustomState('hello')).toBeFalse()
+    })
+    it('> should return true on valid custom state', () => {
+      expect(verifyCustomState('x-test')).toBeTrue()
+    })
+  })
+
+  describe('> encodeCustomState', () => {
+    describe('> malformed values', () => {
+      describe('> bad key', () => {
+        it('> throws', () => {
+          expect(() => {
+            encodeCustomState('hello', 'world')
+          }).toThrow()
+        })
+      })
+      describe('> falsy value', () => {
+        it('> returns falsy value', () => {
+          expect(encodeCustomState('x-test', null)).toBeFalsy()
+        })
+      })
+    })
+    describe('> correct values', () => {
+      it('> encodes correctly', () => {
+        expect(encodeCustomState('x-test', 'foo-bar')).toEqual('x-test:foo-bar')
+      })
+    })
+  })
 })
diff --git a/src/routerModule/util.ts b/src/routerModule/util.ts
index bbf8791d3e53851ffb05a9e0acff9646003ff356..39ec477aebc60ba1bd201971923bc5c3a6f539c5 100644
--- a/src/routerModule/util.ts
+++ b/src/routerModule/util.ts
@@ -306,6 +306,34 @@ export const cvtStateToHashedRoutes = (state): string => {
     : `${routesArr.join('/')}?${searchParam.toString()}`
 }
 
+export const verifyCustomState = (key: string) => {
+  return /^x-/.test(key)
+}
+
+export const decodeCustomState = (fullPath: UrlTree) => {
+  const returnObj: Record<string, string> = {}
+  
+  const pathFragments: UrlSegment[] = fullPath.root.hasChildren()
+    ? fullPath.root.children['primary'].segments
+    : []
+  
+  for (const f of pathFragments) {
+    if (!verifyCustomState(f.path)) continue
+    const { key, val } = decodePath(f.path) || {}
+    if (!key || !val) continue
+    returnObj[key] = val[0]
+  }
+  return returnObj
+}
+
+export const encodeCustomState = (key: string, value: string) => {
+  if (!verifyCustomState(key)) {
+    throw new Error(`custom state must start with x-`)
+  }
+  if (!value) return null
+  return endcodePath(key, value)
+}
+
 @Component({
   template: ''
 })
diff --git a/src/share/saneUrl/saneUrl.component.spec.ts b/src/share/saneUrl/saneUrl.component.spec.ts
index 4b830bb1aac5339f3a27cf9963328be5d9cd45c1..5ab173950c92148144b7b3025f63cddf95205a30 100644
--- a/src/share/saneUrl/saneUrl.component.spec.ts
+++ b/src/share/saneUrl/saneUrl.component.spec.ts
@@ -1,26 +1,49 @@
-import { TestBed, async, fakeAsync, tick, flush } from '@angular/core/testing'
+import { TestBed, fakeAsync, tick, flush } from '@angular/core/testing'
 import { ShareModule } from '../share.module'
 import { SaneUrl } from './saneUrl.component'
 import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'
 import { By } from '@angular/platform-browser'
 import { BACKENDURL } from 'src/util/constants'
 import { NoopAnimationsModule } from '@angular/platform-browser/animations'
+import { SaneUrlSvc } from './saneUrl.service'
+import { AngularMaterialModule } from 'src/sharedModules'
+import { CUSTOM_ELEMENTS_SCHEMA, Directive } from '@angular/core'
+import { of } from 'rxjs'
 
 const inputCss = `input[aria-label="Custom link"]`
 const submitCss = `button[aria-label="Create custom link"]`
 const copyBtnCss = `button[aria-label="Copy created custom URL to clipboard"]`
 
+@Directive({
+  selector: '[iav-auth-auth-state]',
+  exportAs: 'iavAuthAuthState'
+})
+
+class AuthStateDummy {
+  user$ = of(null)
+}
+
 describe('> saneUrl.component.ts', () => {
   describe('> SaneUrl', () => {
-    beforeEach(async(() => {
-      TestBed.configureTestingModule({
+    beforeEach(async () => {
+      await TestBed.configureTestingModule({
         imports: [
-          ShareModule,
           HttpClientTestingModule,
           NoopAnimationsModule,
+          AngularMaterialModule,
+        ],
+        providers: [
+          SaneUrlSvc,
+        ],
+        declarations: [
+          SaneUrl,
+          AuthStateDummy
+        ],
+        schemas: [
+          CUSTOM_ELEMENTS_SCHEMA
         ]
       }).compileComponents()
-    }))
+    })
 
     afterEach(() => {
       const ctrl = TestBed.inject(HttpTestingController)
@@ -65,8 +88,6 @@ describe('> saneUrl.component.ts', () => {
       fixture.detectChanges()
 
       const input = fixture.debugElement.query( By.css( inputCss ) )
-      const invalid = input.attributes['aria-invalid']
-      expect(invalid.toString()).toEqual('true')
 
     }))
 
@@ -89,8 +110,6 @@ describe('> saneUrl.component.ts', () => {
       fixture.detectChanges()
 
       const input = fixture.debugElement.query( By.css( inputCss ) )
-      const invalid = input.attributes['aria-invalid']
-      expect(invalid.toString()).toEqual('false')
     })
 
     it('> on entering string in input, makes debounced GET request', fakeAsync(() => {
@@ -139,8 +158,6 @@ describe('> saneUrl.component.ts', () => {
       fixture.detectChanges()
 
       const input = fixture.debugElement.query( By.css( inputCss ) )
-      const invalid = input.attributes['aria-invalid']
-      expect(invalid.toString()).toEqual('true')
 
       const submit = fixture.debugElement.query( By.css( submitCss ) )
       const disabled = !!submit.attributes['disabled']
@@ -173,8 +190,6 @@ describe('> saneUrl.component.ts', () => {
       fixture.detectChanges()
 
       const input = fixture.debugElement.query( By.css( inputCss ) )
-      const invalid = input.attributes['aria-invalid']
-      expect(invalid.toString()).toEqual('false')
 
       const submit = fixture.debugElement.query( By.css( submitCss ) )
       const disabled = !!submit.attributes['disabled']
@@ -207,8 +222,6 @@ describe('> saneUrl.component.ts', () => {
       fixture.detectChanges()
 
       const input = fixture.debugElement.query( By.css( inputCss ) )
-      const invalid = input.attributes['aria-invalid']
-      expect(invalid.toString()).toEqual('true')
 
       const submit = fixture.debugElement.query( By.css( submitCss ) )
       const disabled = !!submit.attributes['disabled']
@@ -307,8 +320,6 @@ describe('> saneUrl.component.ts', () => {
       fixture.detectChanges()
 
       const input = fixture.debugElement.query( By.css( inputCss ) )
-      const invalid = input.attributes['aria-invalid']
-      expect(invalid.toString()).toEqual('true')
 
     }))
   })
diff --git a/src/share/saneUrl/saneUrl.component.ts b/src/share/saneUrl/saneUrl.component.ts
index c397478f5b62b0a7574d3b4b00a5d95fc8e7253a..dbcbb722efd73c5d9f8a15dc4e6c282901ff7cfa 100644
--- a/src/share/saneUrl/saneUrl.component.ts
+++ b/src/share/saneUrl/saneUrl.component.ts
@@ -1,12 +1,12 @@
 import { Component, OnDestroy, Input } from "@angular/core";
-import { HttpClient } from '@angular/common/http'
-import { BACKENDURL } from 'src/util/constants'
 import { Observable, merge, of, Subscription, BehaviorSubject, combineLatest } from "rxjs";
 import { startWith, mapTo, map, debounceTime, switchMap, catchError, shareReplay, filter, tap, takeUntil, distinctUntilChanged } from "rxjs/operators";
 import { FormControl } from "@angular/forms";
 import { ErrorStateMatcher } from "@angular/material/core";
 import { Clipboard } from "@angular/cdk/clipboard";
 import { MatSnackBar } from "@angular/material/snack-bar";
+import { SaneUrlSvc } from "./saneUrl.service";
+import { NotFoundError } from "../type";
 
 export class SaneUrlErrorStateMatcher implements ErrorStateMatcher{
   isErrorState(ctrl: FormControl | null){
@@ -43,6 +43,8 @@ enum ESavingStatus {
 
 export class SaneUrl implements OnDestroy{
 
+  public saneUrlRoot = this.svc.saneUrlRoot
+
   @Input() stateTobeSaved: any
 
   private subscriptions: Subscription[] = []
@@ -62,9 +64,9 @@ export class SaneUrl implements OnDestroy{
   public saved$: Observable<boolean>
 
   constructor(
-    private http: HttpClient,
     private clipboard: Clipboard,
     private snackbar: MatSnackBar,
+    private svc: SaneUrlSvc,
   ){
 
     const validatedValueInput$ = this.customUrl.valueChanges.pipe(
@@ -84,11 +86,10 @@ export class SaneUrl implements OnDestroy{
       debounceTime(500),
       switchMap(val => val === ''
         ? of(false)
-        : this.http.get(`${this.saneUrlRoot}${val}`).pipe(
+        : this.svc.getKeyVal(val).pipe(
           mapTo(false),
           catchError((err, obs) => {
-            const { status } = err
-            if (status === 404) return of(true)
+            if (err instanceof NotFoundError) return of(true)
             return of(false)
           })
         )
@@ -105,10 +106,10 @@ export class SaneUrl implements OnDestroy{
       )
     )
 
-    this.btnHintTxt$ = combineLatest(
+    this.btnHintTxt$ = combineLatest([
       this.savingStatus$,
       this.savingProgress$,
-    ).pipe(
+    ]).pipe(
       map(([savingStatus, savingProgress]) => {
         if (savingProgress === ESavingProgress.DONE) return EBtnTxt.CREATED
         if (savingProgress === ESavingProgress.INPROGRESS) return EBtnTxt.CREATING
@@ -123,10 +124,10 @@ export class SaneUrl implements OnDestroy{
       startWith(true)
     )
 
-    this.iconClass$ = combineLatest(
+    this.iconClass$ = combineLatest([
       this.savingStatus$,
       this.savingProgress$,
-    ).pipe(
+    ]).pipe(
       map(([savingStatus, savingProgress]) => {
         if (savingProgress === ESavingProgress.DONE) return `fas fa-check`
         if (savingProgress === ESavingProgress.INPROGRESS) return `fas fa-spinner fa-spin`
@@ -159,8 +160,8 @@ export class SaneUrl implements OnDestroy{
   saveLink(){
     this.savingProgress$.next(ESavingProgress.INPROGRESS)
     this.customUrl.disable()
-    this.http.post(
-      `${this.saneUrlRoot}${this.customUrl.value}`,
+    this.svc.setKeyVal(
+      this.customUrl.value,
       this.stateTobeSaved
     ).subscribe(
       resp => {
@@ -184,6 +185,4 @@ export class SaneUrl implements OnDestroy{
       { duration: 1000 }
     )
   }
-
-  public saneUrlRoot = `${BACKENDURL}saneUrl/`
 }
diff --git a/src/share/saneUrl/saneUrl.service.ts b/src/share/saneUrl/saneUrl.service.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8f3af1f3d55fcb9baec57d44ddbbda76ac24c2df
--- /dev/null
+++ b/src/share/saneUrl/saneUrl.service.ts
@@ -0,0 +1,43 @@
+import { HttpClient } from "@angular/common/http";
+import { Injectable } from "@angular/core";
+import { throwError } from "rxjs";
+import { catchError, mapTo } from "rxjs/operators";
+import { BACKENDURL } from 'src/util/constants'
+import { IKeyValStore, NotFoundError } from '../type'
+
+@Injectable({
+  providedIn: 'root'
+})
+
+export class SaneUrlSvc implements IKeyValStore{
+  public saneUrlRoot = `${BACKENDURL}saneUrl/`
+  constructor(
+    private http: HttpClient
+  ){
+
+  }
+
+  getKeyVal(key: string) {
+    return this.http.get<Record<string, any>>(
+      `${this.saneUrlRoot}${key}`,
+      { responseType: 'json' }
+    ).pipe(
+      catchError((err, obs) => {
+        const { status } = err
+        if (status === 404) {
+          return throwError(new NotFoundError('Not found'))
+        }
+        return throwError(err)
+      })
+    )
+  }
+
+  setKeyVal(key: string, value: any) {
+    return this.http.post(
+      `${this.saneUrlRoot}${key}`,
+      value
+    ).pipe(
+      mapTo(`${this.saneUrlRoot}${key}`)
+    )
+  }
+}
diff --git a/src/share/saneUrl/saneUrl.template.html b/src/share/saneUrl/saneUrl.template.html
index a673dcef78ef91587d47cf9406af51af7de8b151..cd191f957b3e6e59a687bd37593d5058238cfed0 100644
--- a/src/share/saneUrl/saneUrl.template.html
+++ b/src/share/saneUrl/saneUrl.template.html
@@ -1,3 +1,36 @@
+<div iav-auth-auth-state
+  #authState="iavAuthAuthState">
+
+<!-- Logged in. Explain that links will not expire, offer to logout -->
+<ng-container *ngIf="authState.user$ | async as user; else otherTmpl">
+  <span>
+    Logged in as {{ user.name }}
+  </span>
+  <button mat-button
+    color="warn"
+    tabindex="-1">
+    <i class="fas fa-sign-in-alt"></i>
+    <span>
+      Logout
+    </span>
+  </button>
+</ng-container>
+
+<!-- Not logged in. Offer to login -->
+<ng-template #otherTmpl>
+  <span>
+    Not logged in
+  </span>
+  <signin-modal></signin-modal>
+</ng-template>
+</div>
+
+<!-- explain links expiration -->
+<div class="text-muted mat-small">
+{{ (authState.user$ | async) ? 'Links you generate will not expire' : 'Links you generate will expire after 72 hours' }}
+</div>
+
+
 <mat-form-field class="mr-2">
   <span matPrefix class="text-muted">
     {{ saneUrlRoot }}
diff --git a/src/share/share.module.ts b/src/share/share.module.ts
index 28e16704f18648e8a4a554d1ad63d0f50e14a1c6..9634e12279dd09f165c6a6d4fd34653cbf52550f 100644
--- a/src/share/share.module.ts
+++ b/src/share/share.module.ts
@@ -5,6 +5,7 @@ import { HttpClientModule } from "@angular/common/http";
 import { SaneUrl } from "./saneUrl/saneUrl.component";
 import { CommonModule } from "@angular/common";
 import { ReactiveFormsModule, FormsModule } from "@angular/forms";
+import { AuthModule } from "src/auth";
 
 @NgModule({
   imports: [
@@ -13,6 +14,7 @@ import { ReactiveFormsModule, FormsModule } from "@angular/forms";
     CommonModule,
     FormsModule,
     ReactiveFormsModule,
+    AuthModule,
   ],
   declarations: [
     ClipboardCopy,
diff --git a/src/share/type.ts b/src/share/type.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ead6dfdbdb8c409f6051532bfec218cda0625d46
--- /dev/null
+++ b/src/share/type.ts
@@ -0,0 +1,8 @@
+import { Observable } from "rxjs";
+
+export class NotFoundError extends Error{}
+
+export interface IKeyValStore {
+  getKeyVal(key: string): Observable<unknown>
+  setKeyVal(key: string, val: unknown): Observable<void|string>
+}
diff --git a/src/state/stateAggregator.directive.ts b/src/state/stateAggregator.directive.ts
index 62d66666227e91a18bd1aac9ba2309d7a5c43a8d..28df9bf19c5000333fbe228743b1495932f117fd 100644
--- a/src/state/stateAggregator.directive.ts
+++ b/src/state/stateAggregator.directive.ts
@@ -1,14 +1,14 @@
-import { Directive } from "@angular/core";
+import { Directive, OnDestroy } from "@angular/core";
 import { NavigationEnd, Router } from "@angular/router";
-import { Observable } from "rxjs";
-import { filter, map } from "rxjs/operators";
+import { Observable, Subscription } from "rxjs";
+import { filter, map, startWith } from "rxjs/operators";
 
 const jsonVersion = '1.0.0'
 // ver 0.0.1 === query param
 
 interface IJsonifiedState {
   ver: string
-  queryString: any
+  hashPath: string
 }
 
 @Directive({
@@ -16,20 +16,30 @@ interface IJsonifiedState {
   exportAs: 'iavStateAggregator'
 })
 
-export class StateAggregator{
+export class StateAggregator implements OnDestroy{
 
-  public jsonifiedSstate$: Observable<IJsonifiedState>
+  public jsonifiedState: IJsonifiedState
+  public jsonifiedState$: Observable<IJsonifiedState> = this.router.events.pipe(
+    filter(ev => ev instanceof NavigationEnd),
+    map((ev: NavigationEnd) => ev.urlAfterRedirects),
+    startWith(this.router.url),
+    map((path: string) => {
+      return {
+        ver: jsonVersion,
+        hashPath: path
+      }
+    }),
+  )
   constructor(
-    router: Router
+    private router: Router
   ){
-    this.jsonifiedSstate$ = router.events.pipe(
-      filter(ev => ev instanceof NavigationEnd),
-      map((ev: NavigationEnd) => {
-        return {
-          ver: jsonVersion,
-          queryString: ev.urlAfterRedirects
-        }
-      })
+    this.subscriptions.push(
+      this.jsonifiedState$.subscribe(val => this.jsonifiedState = val)
     )
   }
+
+  private subscriptions: Subscription[] = []
+  ngOnDestroy(){
+    while (this.subscriptions.length) this.subscriptions.pop().unsubscribe()
+  }
 }
diff --git a/src/util/mergeObj.pipe.ts b/src/util/mergeObj.pipe.ts
new file mode 100644
index 0000000000000000000000000000000000000000..104592d6732bcadff54c8a5a92a32405e40f6025
--- /dev/null
+++ b/src/util/mergeObj.pipe.ts
@@ -0,0 +1,17 @@
+import { Pipe, PipeTransform } from "@angular/core";
+
+type TObj = Record<string, any>
+
+@Pipe({
+  name: 'mergeObj',
+  pure: true
+})
+
+export class MergeObjPipe implements PipeTransform{
+  public transform(o1: TObj, o2: TObj){
+    return {
+      ...o1,
+      ...o2
+    }
+  }
+}
diff --git a/src/util/util.module.ts b/src/util/util.module.ts
index 4d817736e98a231bfde294dd19b751fd44f3db63..bde4b907f1d01f911d5d9886ed967ba5b4698f3f 100644
--- a/src/util/util.module.ts
+++ b/src/util/util.module.ts
@@ -20,6 +20,7 @@ import { GetPropertyPipe } from "./pipes/getProperty.pipe";
 import { FilterArrayPipe } from "./pipes/filterArray.pipe";
 import { DoiParserPipe } from "./pipes/doiPipe.pipe";
 import { GetFilenamePipe } from "./pipes/getFilename.pipe";
+import { MergeObjPipe } from "./mergeObj.pipe";
 
 @NgModule({
   imports:[
@@ -45,6 +46,7 @@ import { GetFilenamePipe } from "./pipes/getFilename.pipe";
     FilterArrayPipe,
     DoiParserPipe,
     GetFilenamePipe,
+    MergeObjPipe,
   ],
   exports: [
     FilterRowsByVisbilityPipe,
@@ -66,6 +68,7 @@ import { GetFilenamePipe } from "./pipes/getFilename.pipe";
     FilterArrayPipe,
     DoiParserPipe,
     GetFilenamePipe,
+    MergeObjPipe,
   ]
 })
 
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.component.ts b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
index 76dfa6bbbdb0288b6755984fa651d71b3e50c5df..a6daa233d6acdc5197d19d4a9dbc284c3e26f329 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.component.ts
+++ b/src/viewerModule/nehuba/statusCard/statusCard.component.ts
@@ -57,8 +57,6 @@ export class StatusCardComponent implements OnInit, OnChanges{
     order: 6,
   }
 
-  public saneUrlDeprecated = `Custom URL is going away. New custom URLs can no longer be created. Custom URLs you generated in the past will continue to work.`
-
   public SHARE_BTN_ARIA_LABEL = ARIA_LABELS.SHARE_BTN
   public COPY_URL_TO_CLIPBOARD_ARIA_LABEL = ARIA_LABELS.SHARE_COPY_URL_CLIPBOARD
   public SHARE_CUSTOM_URL_ARIA_LABEL = ARIA_LABELS.SHARE_CUSTOM_URL
diff --git a/src/viewerModule/nehuba/statusCard/statusCard.template.html b/src/viewerModule/nehuba/statusCard/statusCard.template.html
index 5f57d9719055a265dbfb005c5c4d7d3cc46ab1ea..69d2a44547d2761e2c20c96f5a7d1bb155dcf535 100644
--- a/src/viewerModule/nehuba/statusCard/statusCard.template.html
+++ b/src/viewerModule/nehuba/statusCard/statusCard.template.html
@@ -157,8 +157,9 @@
     <mat-list-item
       [attr.aria-label]="SHARE_CUSTOM_URL_ARIA_LABEL"
       [attr.tab-index]="10"
-      [matTooltip]="saneUrlDeprecated"
-      class="text-muted">
+      (click)="openDialog(shareSaneUrl, { ariaLabel: SHARE_CUSTOM_URL_ARIA_LABEL })"
+      [matTooltip]="SHARE_CUSTOM_URL_ARIA_LABEL"
+      >
       <mat-icon
         class="mr-4"
         fontSet="fas"
@@ -179,40 +180,8 @@
   </h2>
 
   <div mat-dialog-content>
-    <div iav-auth-auth-state
-      #authState="iavAuthAuthState">
-
-      <!-- Logged in. Explain that links will not expire, offer to logout -->
-      <ng-container *ngIf="authState.user$ | async as user; else otherTmpl">
-        <span>
-          Logged in as {{ user.name }}
-        </span>
-        <button mat-button
-          color="warn"
-          tabindex="-1">
-          <i class="fas fa-sign-in-alt"></i>
-          <span>
-            Logout
-          </span>
-        </button>
-      </ng-container>
-
-      <!-- Not logged in. Offer to login -->
-      <ng-template #otherTmpl>
-        <span>
-          Not logged in
-        </span>
-        <signin-modal></signin-modal>
-      </ng-template>
-    </div>
-
-    <!-- explain links expiration -->
-    <div class="text-muted mat-small">
-      {{ (authState.user$ | async) ? 'Links you generate will not expire' : 'Links you generate will expire after 72 hours' }}
-    </div>
-
     <iav-sane-url iav-state-aggregator
-      [stateTobeSaved]="stateAggregator.jsonifiedSstate$ | async"
+      [stateTobeSaved]="stateAggregator.jsonifiedState$ | async"
       #stateAggregator="iavStateAggregator">
     </iav-sane-url>
   </div>