diff --git a/src/api/allowedMetadata.ts b/src/api/allowedMetadata.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8156aa61ecbd21f2cbbc01f1666c17a45ba596d5
--- /dev/null
+++ b/src/api/allowedMetadata.ts
@@ -0,0 +1,37 @@
+import axios from 'axios'
+import { PageNumberPaginationParameters, unique } from '.'
+import { AllowedMetaData, PageNumberPagination, UUID } from '@/types'
+
+export interface AllowedMetaDataParameters extends PageNumberPaginationParameters {
+  /**
+   * ID of the corpus to retrieve allowed metadata from.
+   */
+  id: UUID
+}
+
+interface AllowedMetaDataCreate extends Omit<AllowedMetaData, 'id'> {
+  corpusId: UUID
+}
+
+// List metadata type-name couples allowed for manual annotation
+export const listCorpusAllowedMetadata = unique(
+  async ({ id, ...params }: AllowedMetaDataParameters): Promise<PageNumberPagination<AllowedMetaData>> =>
+    (await axios.get(`/corpus/${id}/allowed-metadata/`, { params })).data
+)
+
+// Create a new metadata type-name couple allowed in a corpus.
+export const createAllowedMetadata = unique(
+  async ({ corpusId, ...data }: AllowedMetaDataCreate): Promise<AllowedMetaData> =>
+    (await axios.post(`/corpus/${corpusId}/allowed-metadata/`, data)).data
+)
+
+// Delete a metadata type-name couple from the list of metadata allowed in a corpus.
+export const deleteAllowedMetadata = unique(
+  async (corpusId: UUID, mdId: UUID) => await axios.delete(`/corpus/${corpusId}/allowed-metadata/${mdId}/`)
+)
+
+// Update an existing metadata type-name couple allowed in a corpus.
+export const updateAllowedMetadata = unique(
+  async (corpusId: UUID, allowedMetaDataId: UUID, data: Omit<AllowedMetaData, 'id'>): Promise<AllowedMetaData> =>
+    (await axios.patch(`/corpus/${corpusId}/allowed-metadata/${allowedMetaDataId}/`, data)).data
+)
diff --git a/src/api/index.ts b/src/api/index.ts
index a38547c72f4b1274abf4033cffcf2b662a47e4ae..f766eea1b95e7d269ea511ff3bd9870c198a5fde 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -14,7 +14,7 @@ export * from './files'
 export * from './image'
 export * from './ingest'
 export * from './job'
-export * from './metadata'
+export * from './allowedMetadata'
 export * from './mlclass'
 export * from './mlresults'
 export * from './model'
@@ -30,6 +30,7 @@ export * from './worker'
 export * from './workerActivity'
 export * from './workerConfiguration'
 export * from './workerRun'
+export * from './metadata'
 
 /**
  * Prevent running more than one promise at once on an asynchronous function.
diff --git a/src/api/metadata.ts b/src/api/metadata.ts
index b96e7041e1f8bcb9067897a866da3605337fe9b0..ddcc0f7d6273e1af79cfa13dc178bc9406df706c 100644
--- a/src/api/metadata.ts
+++ b/src/api/metadata.ts
@@ -1,44 +1,6 @@
 import axios from 'axios'
-import { PageNumberPaginationParameters, unique } from '.'
-import { AllowedMetaData, MetaData, MetaDataCreate, TypedMetaData, PageNumberPagination, UUID } from '@/types'
-
-export interface AllowedMetaDataParameters extends PageNumberPaginationParameters {
-  /**
-   * ID of the corpus to retrieve allowed metadata from.
-   */
-  id: UUID
-}
-
-interface AllowedMetaDataCreate extends Omit<AllowedMetaData, 'id'> {
-  corpusId: UUID
-}
-
-interface AllowedMetaDataEdit extends AllowedMetaData {
-  corpusId: UUID
-}
-
-// List metadata type-name couples allowed for manual annotation
-export const listCorpusAllowedMetadata = unique(
-  async ({ id, ...params }: AllowedMetaDataParameters): Promise<PageNumberPagination<AllowedMetaData>> =>
-    (await axios.get(`/corpus/${id}/allowed-metadata/`, { params })).data
-)
-
-// Create a new metadata type-name couple allowed in a corpus.
-export const createAllowedMetadata = unique(
-  async ({ corpusId, ...data }: AllowedMetaDataCreate): Promise<AllowedMetaData> =>
-    (await axios.post(`/corpus/${corpusId}/allowed-metadata/`, data)).data
-)
-
-// Delete a metadata type-name couple from the list of metadata allowed in a corpus.
-export const deleteAllowedMetadata = unique(
-  async (corpusId: UUID, mdId: UUID) => await axios.delete(`/corpus/${corpusId}/allowed-metadata/${mdId}/`)
-)
-
-// Update an existing metadata type-name couple allowed in a corpus.
-export const updateAllowedMetadata = unique(
-  async ({ corpusId, id, ...data }: AllowedMetaDataEdit): Promise<AllowedMetaData> =>
-    (await axios.patch(`/corpus/${corpusId}/allowed-metadata/${id}/`, data)).data
-)
+import { unique } from '.'
+import { MetaData, MetaDataCreate, TypedMetaData, UUID } from '@/types'
 
 // Create a metadata.
 export const createMetadata = unique(
diff --git a/src/components/Corpus/AllowedMetaData/CreateForm.vue b/src/components/Corpus/AllowedMetaData/CreateForm.vue
index 126ed24bafca637e98d19e6656d46b51b06c6a17..3d53631832594f77552da0378960590285f415d7 100644
--- a/src/components/Corpus/AllowedMetaData/CreateForm.vue
+++ b/src/components/Corpus/AllowedMetaData/CreateForm.vue
@@ -4,7 +4,7 @@
       <input
         class="input"
         type="text"
-        :disabled="!canAdmin(corpus) || loading || null"
+        :disabled="!canAdmin(corpus) || loading || undefined"
         v-model="createMeta.name"
         required
       />
@@ -17,7 +17,7 @@
         <div class="select">
           <select
             v-model="createMeta.type"
-            :disabled="!canAdmin(corpus) || loading || null"
+            :disabled="!canAdmin(corpus) || loading || undefined"
             required
           >
             <option
@@ -45,7 +45,7 @@
       <button
         class="button is-primary"
         v-on:click="create"
-        :disabled="!allowCreate || null"
+        :disabled="!allowCreate || undefined"
         :title="allowCreate ? 'Create allowed metadata' : createDisabledTitle"
       >
         <i class="icon-plus"></i>
@@ -54,17 +54,23 @@
   </tr>
 </template>
 
-<script>
-import { mapMutations } from 'vuex'
+<script lang='ts'>
+import { defineComponent, PropType } from 'vue'
+import { mapActions } from 'pinia'
 import { corporaMixin } from '@/mixins.js'
-import { METADATA_TYPES } from '@/config'
-export default {
+import { METADATA_TYPES, UUID_REGEX } from '@/config'
+import { useAllowedMetaDataStore, useNotificationStore } from '@/stores'
+import { isAxiosError } from 'axios'
+import { UUID } from '@/types'
+
+export default defineComponent({
   mixins: [
     corporaMixin
   ],
   props: {
     corpusId: {
-      type: String,
+      type: String as PropType<UUID>,
+      validator: value => typeof value === 'string' && UUID_REGEX.test(value),
       required: true
     }
   },
@@ -98,13 +104,13 @@ export default {
     }
   },
   methods: {
-    ...mapMutations('notifications', ['notify']),
+    ...mapActions(useNotificationStore, ['notify']),
+    ...mapActions(useAllowedMetaDataStore, ['createAllowedMetadata']),
     async create () {
       if (!this.allowCreate) return
-
       this.loading = true
       try {
-        await this.$store.dispatch('corpora/createAllowedMetadata', { corpusId: this.corpusId, ...this.createMeta })
+        await this.createAllowedMetadata(this.corpusId, this.createMeta)
         this.createMeta = {
           name: '',
           type: ''
@@ -115,13 +121,13 @@ export default {
         this.createMeta.name = ''
         this.createMeta.type = ''
       } catch (err) {
-        if (err.response.status === 400) this.createErrors = err.response.data
+        if (isAxiosError(err) && err.response && err.response.status === 400) this.createErrors = err.response.data
       } finally {
         this.loading = false
       }
     }
   }
-}
+})
 </script>
 
 <style scoped>
diff --git a/src/components/Corpus/AllowedMetaData/List.vue b/src/components/Corpus/AllowedMetaData/List.vue
index 0cc7d950d3c5dd30d7f83b0f39713f206d70b58d..0d1e9982ae7f680b9a1e8412fac6bb5abdc2921c 100644
--- a/src/components/Corpus/AllowedMetaData/List.vue
+++ b/src/components/Corpus/AllowedMetaData/List.vue
@@ -9,7 +9,7 @@
     </thead>
     <tbody>
       <MetaData
-        v-for="metaData in corpusAllowedMetadata[corpusId]"
+        v-for="metaData in allowedMetadata[corpusId]"
         :key="metaData.name"
         :metadata="metaData"
         :corpus-id="corpusId"
@@ -21,20 +21,25 @@
   </table>
 </template>
 
-<script>
-import { mapState, mapActions, mapMutations } from 'vuex'
+<script lang='ts'>
+import { mapState, mapActions } from 'pinia'
+import { defineComponent, PropType } from 'vue'
 import { errorParser } from '@/helpers'
-import MetaData from './Row'
-import CreateForm from './CreateForm'
+import MetaData from './Row.vue'
+import CreateForm from './CreateForm.vue'
+import { useNotificationStore, useAllowedMetaDataStore } from '@/stores'
+import { UUID } from '@/types'
+import { UUID_REGEX } from '@/config'
 
-export default {
+export default defineComponent({
   components: {
     MetaData,
     CreateForm
   },
   props: {
     corpusId: {
-      type: String,
+      type: String as PropType<UUID>,
+      validator: value => typeof value === 'string' && UUID_REGEX.test(value),
       required: true
     }
   },
@@ -42,16 +47,16 @@ export default {
     loading: false
   }),
   computed: {
-    ...mapState('corpora', ['corpusAllowedMetadata'])
+    ...mapState(useAllowedMetaDataStore, ['allowedMetadata'])
   },
   methods: {
-    ...mapActions('corpora', ['listAllowedMetadata']),
-    ...mapMutations('notifications', ['notify']),
+    ...mapActions(useAllowedMetaDataStore, ['listAllowedMetadata']),
+    ...mapActions(useNotificationStore, ['notify']),
     async listMetaData () {
-      if (this.corpusAllowedMetadata[this.corpusId]) return
+      if (this.allowedMetadata[this.corpusId]) return
       this.loading = true
       try {
-        await this.listAllowedMetadata({ corpusId: this.corpusId })
+        await this.listAllowedMetadata(this.corpusId)
       } catch (err) {
         this.notify({ type: 'error', text: `An error occurred listing allowed metadata: ${errorParser(err)}` })
       } finally {
@@ -65,5 +70,5 @@ export default {
       immediate: true
     }
   }
-}
+})
 </script>
diff --git a/src/components/Corpus/AllowedMetaData/Row.vue b/src/components/Corpus/AllowedMetaData/Row.vue
index 9cb1a1a068fe2a976de5799910583b79ebb4259c..4a437874dc6096c7e850435c4dff8113d2d81fb2 100644
--- a/src/components/Corpus/AllowedMetaData/Row.vue
+++ b/src/components/Corpus/AllowedMetaData/Row.vue
@@ -1,12 +1,12 @@
 <template>
   <tr v-if="metadata">
     <!-- edit mode -->
-    <template v-if="editing" v-on:submit.prevent="save">
+    <template v-if="editing">
       <td>
         <input
           class="input"
           type="text"
-          :disabled="loading || null"
+          :disabled="loading || undefined"
           v-model="editMeta.name"
           required
         />
@@ -19,7 +19,7 @@
           <div class="select">
             <select
               v-model="editMeta.type"
-              :disabled="loading || null"
+              :disabled="loading || undefined"
               required
             >
               <option
@@ -45,7 +45,7 @@
       </td>
     </template>
     <!-- display mode -->
-    <template v-else v-on:submit.prevent="save">
+    <template v-else>
       <td>{{ metadata.name }}</td>
       <td>{{ METADATA_TYPES[metadata.type].display }}</td>
     </template>
@@ -66,7 +66,7 @@
           v-else
           class="button mr-2"
           type="submit"
-          :disabled="!allowEdit || null"
+          :disabled="!allowEdit || undefined"
           :title="allowEdit ? 'Edit allowed metadata' : 'You must have an admin access to this project in order to perform this action.'"
           v-on:click="edit(metadata)"
         >
@@ -74,7 +74,7 @@
         </button>
         <button
           class="button has-text-danger"
-          :disabled="!allowDelete || null"
+          :disabled="!allowDelete || undefined"
           v-on:click="deleteModal = allowDelete"
           :title="
             allowDelete
@@ -106,12 +106,17 @@
   </tr>
 </template>
 
-<script>
-import { mapActions } from 'vuex'
+<script lang='ts'>
+import { defineComponent, PropType } from 'vue'
+import { mapActions } from 'pinia'
 import Modal from '@/components/Modal.vue'
-import { METADATA_TYPES } from '@/config'
+import { METADATA_TYPES, UUID_REGEX } from '@/config'
 import { corporaMixin } from '@/mixins.js'
-export default {
+import { useAllowedMetaDataStore } from '@/stores'
+import { AllowedMetaData, UUID } from '@/types'
+import { isAxiosError } from 'axios'
+
+export default defineComponent({
   mixins: [
     corporaMixin
   ],
@@ -120,11 +125,12 @@ export default {
   },
   props: {
     corpusId: {
-      type: String,
+      type: String as PropType<UUID>,
+      validator: value => typeof value === 'string' && UUID_REGEX.test(value),
       required: true
     },
     metadata: {
-      type: Object,
+      type: Object as PropType<AllowedMetaData>,
       required: true
     }
   },
@@ -156,8 +162,7 @@ export default {
         this.editMeta.name.trim() !== this.metadata.name)
     },
     disableButton () {
-      // disabled must be set to null, undefined, or an empty string to not actually disable.
-      let disable = null
+      let disable = false
       if (!this.allowEdit) disable = true
       else if (this.editing && !this.allowUpdate) disable = true
       return disable
@@ -179,7 +184,8 @@ export default {
     }
   },
   methods: {
-    edit (metadata) {
+    ...mapActions(useAllowedMetaDataStore, ['deleteAllowedMetadata', 'updateAllowedMetadata']),
+    edit (metadata: AllowedMetaData) {
       this.editing = true
       this.editMeta.name = metadata.name
       this.editMeta.type = metadata.type
@@ -188,31 +194,32 @@ export default {
       if (!this.allowUpdate) return
       this.loading = true
       try {
-        await this.$store.dispatch('corpora/updateAllowedMetadata', { corpusId: this.corpusId, mdId: this.metadata.id, ...this.editMeta })
+        await this.updateAllowedMetadata(this.corpusId, { id: this.metadata.id, ...this.editMeta })
         this.editMeta = {
           type: '',
           name: ''
         }
       } catch (err) {
-        this.updateErrors = err.response.data
+        if (isAxiosError(err) && err.response) {
+          this.updateErrors = err.response.data
+        }
       } finally {
         this.loading = false
         this.editing = false
       }
     },
-    ...mapActions('corpora', ['deleteAllowedMetadata']),
     async mdDelete () {
       if (!this.allowDelete) return
       this.deleteLoading = true
       try {
-        await this.deleteAllowedMetadata({ corpusId: this.corpusId, mdId: this.metadata.id })
+        await this.deleteAllowedMetadata(this.corpusId, this.metadata.id)
         this.deleteModal = false
       } finally {
         this.deleteLoading = false
       }
     }
   }
-}
+})
 </script>
 
 <style scoped>
diff --git a/src/components/Element/Metadata/Metadata.vue b/src/components/Element/Metadata/Metadata.vue
index 36c2d6afe0ae829887fdc92eefec8f714eb6399d..654003530ffde46817720b18301b0fc7cc0b1c98 100644
--- a/src/components/Element/Metadata/Metadata.vue
+++ b/src/components/Element/Metadata/Metadata.vue
@@ -62,7 +62,7 @@
           Please make sure your metadata types and names are correct to preserve data integrity.<br />
           It is highly recommended to define project-wide allowed metadata in the administration panel instead of using this mode.
         </div>
-        <div class="notification is-warning" v-if="!allowedMetadata.length && !freeEdit">
+        <div class="notification is-warning" v-if="!corpusAllowedMetadata.length && !freeEdit">
           There are no allowed metadata for this project.
           {{ isAdmin ? 'Use the administration panel to add them.' : 'Please contact your administrator.' }}
         </div>
@@ -109,7 +109,7 @@
                 <select v-model="selectedAllowedMetadata">
                   <option value="" selected disabled>Select a metadata…</option>
                   <option
-                    v-for="(meta, index) in allowedMetadata"
+                    v-for="(meta, index) in corpusAllowedMetadata"
                     :key="index"
                     :value="index"
                   >
@@ -194,7 +194,7 @@ import {
 
 import { METADATA_TYPES } from '@/config'
 import { corporaMixin } from '@/mixins'
-import { useDisplayStore, useMetaDataStore } from '@/stores'
+import { useDisplayStore, useMetaDataStore, useAllowedMetaDataStore } from '@/stores'
 
 import DateInput from '@/components/DateInput'
 import Modal from '@/components/Modal'
@@ -235,12 +235,12 @@ export default {
   }),
   computed: {
     ...mapVuexGetters('auth', ['isAdmin']),
-    ...mapVuexState('corpora', ['corpusAllowedMetadata']),
+    ...mapState(useAllowedMetaDataStore, ['allowedMetadata']),
     ...mapVuexState('elements', ['elements']),
     ...mapState(useDisplayStore, ['lastMetadataName', 'lastMetadataType', 'dropdowns']),
     ...mapState(useMetaDataStore, ['metadata']),
-    allowedMetadata () {
-      return this.corpusAllowedMetadata[this.corpusId] || []
+    corpusAllowedMetadata () {
+      return this.allowedMetadata[this.corpusId] || []
     },
     element () {
       return this.elements[this.elementId]
@@ -273,12 +273,13 @@ export default {
     ...mapVuexMutations('notifications', ['notify']),
     ...mapActions(useDisplayStore, ['setLastMetadata']),
     ...mapActions(useMetaDataStore, ['updateMetadata', 'createMetadata', 'deleteMetadata', 'listMetadata']),
+    ...mapActions(useAllowedMetaDataStore, ['listAllowedMetadata']),
     isEditable (md) {
       return this.isAdmin || (this.canWrite(this.corpus) && this.isAllowed(md))
     },
     isAllowed (md = this.selectedMetadata) {
       // Check if metadata is part of allowed ones
-      return this.allowedMetadata.some(allowed => allowed.type === md.type && allowed.name === md.name)
+      return this.corpusAllowedMetadata.some(allowed => allowed.type === md.type && allowed.name === md.name)
     },
     validateForm () {
       this.formErrors = {}
@@ -381,7 +382,7 @@ export default {
      * If it is not found, it switches to free edit mode for admins, or unselects.
      */
     findAllowedMetadata ({ type, name }) {
-      const foundIndex = this.allowedMetadata.findIndex(
+      const foundIndex = this.corpusAllowedMetadata.findIndex(
         allowed => allowed.type === type && allowed.name === name
       )
       if (foundIndex < 0) {
@@ -397,7 +398,7 @@ export default {
     },
     updateSelectedAllowedMetadata () {
       if (!this.selectedMetadata || !Number.isInteger(this.selectedAllowedMetadata) || this.selectedAllowedMetadata < 0) return
-      const allowedMetadata = { name: this.allowedMetadata[this.selectedAllowedMetadata].name, type: this.allowedMetadata[this.selectedAllowedMetadata].type }
+      const allowedMetadata = { name: this.corpusAllowedMetadata[this.selectedAllowedMetadata].name, type: this.corpusAllowedMetadata[this.selectedAllowedMetadata].type }
       if (!allowedMetadata) return
       this.selectedMetadata = {
         ...this.selectedMetadata,
@@ -422,8 +423,8 @@ export default {
       immediate: true,
       handler () {
         // Do fetch allowed metadata instantly after retrieving the corpus
-        if (!this.corpus.id || this.allowedMetadata.length) return
-        this.$store.dispatch('corpora/listAllowedMetadata', { corpusId: this.corpusId })
+        if (!this.corpus.id || this.corpusAllowedMetadata.length) return
+        this.listAllowedMetadata(this.corpusId)
       }
     },
     opened: {
diff --git a/src/filterbar.js b/src/filterbar.js
index bc5457346fb98166777762460b7175297eb022db..4c2bd6322d2baf094700782e8d8ca9d91ffa20bf 100644
--- a/src/filterbar.js
+++ b/src/filterbar.js
@@ -10,7 +10,7 @@
  * of their methods and wreaking havoc upon the filter bar.
  */
 import * as api from '@/api'
-import { useMLResultsStore } from '@/stores'
+import { useMLResultsStore, useAllowedMetaDataStore } from '@/stores'
 import { UUID_REGEX } from '@/config'
 
 /**
@@ -325,11 +325,12 @@ export class MetadataNameFilter extends Filter {
    * @returns {Promise<string[]>} Suggested metadata names.
    */
   async autocomplete () {
+    const allowedMetadataStore = useAllowedMetaDataStore()
     const corpusId = this.store.state.navigation.corpusId
     // Load the corpus' allowed metadata if needed
-    await this.store.dispatch('corpora/listAllowedMetadata', { corpusId })
+    await allowedMetadataStore.listAllowedMetadata(corpusId)
     // Retrieve unique metadata names; there might be multiple AllowedMetadata with the same name and different types
-    return [...new Set(this.store.state.corpora.corpusAllowedMetadata[corpusId].map(({ name }) => name))]
+    return [...new Set(allowedMetadataStore.allowedMetadata[corpusId].map(({ name }) => name))]
   }
 }
 
diff --git a/src/store/corpora.js b/src/store/corpora.js
index 91d821e8a9d416ed6a85902cf87652823e157807..0088674aa0539fec04f15e04c87a1fb6552a8514 100644
--- a/src/store/corpora.js
+++ b/src/store/corpora.js
@@ -11,8 +11,6 @@ export const initialState = () => ({
    * Prevents trying to reload the corpora when there are none available.
    */
   corporaLoaded: false,
-  // { [corpusId]: AllowedMetadata[] }
-  corpusAllowedMetadata: {},
   // { [corpusId]: EntityTypes[] }
   corpusEntityTypes: {}
 })
@@ -109,37 +107,6 @@ export const mutations = {
     state.corpusEntityTypes[corpusId] = typesList
   },
 
-  setCorpusAllowedMetadata (state, { corpusId, results }) {
-    const updatedList = state.corpusAllowedMetadata[corpusId] || []
-    results.forEach(newMeta => {
-      // Prevent duplicating allowed metadata
-      if (!updatedList.some(meta => meta.type === newMeta.type && meta.name === newMeta.name)) updatedList.push(newMeta)
-    })
-    // Merge corpus allowed metadata
-    state.corpusAllowedMetadata = {
-      ...state.corpusAllowedMetadata,
-      [corpusId]: updatedList
-    }
-  },
-
-  updateCorpusAllowedMetadata (state, { corpusId, data }) {
-    if (!state.corpusAllowedMetadata[corpusId]) throw new Error(`Allowed metadata for corpus ${corpusId} not found`)
-    const mdList = [...state.corpusAllowedMetadata[corpusId]]
-    const index = mdList.findIndex(item => item.id === data.id)
-    if (index < 0) throw new Error(`Allowed metadata ${data.id} not found in corpus ${corpusId}`)
-    mdList.splice(index, 1, data)
-    state.corpusAllowedMetadata[corpusId] = mdList
-  },
-
-  removeCorpusAllowedMetadata (state, { corpusId, mdId }) {
-    if (!state.corpusAllowedMetadata[corpusId]) throw new Error(`Allowed metadata for corpus ${corpusId} not found`)
-    const mdList = [...state.corpusAllowedMetadata[corpusId]]
-    const index = mdList.findIndex(item => item.id === mdId)
-    if (index < 0) throw new Error(`Allowed metadata ${mdId} not found in corpus ${corpusId}`)
-    mdList.splice(index, 1)
-    state.corpusAllowedMetadata[corpusId] = mdList
-  },
-
   addDefaultCorpus (state, { id, name }) {
     state.corpora = {
       ...state.corpora,
@@ -263,50 +230,6 @@ export const actions = {
     }
   },
 
-  async listAllowedMetadata ({ state, commit, dispatch }, { corpusId, page = 1 }) {
-    // Do not start fetching allowed metadata if they have been retrieved already
-    if (page === 1 && state.corpusAllowedMetadata[corpusId]) return
-    // Automatically list all allowed metadata for a corpus through pagination
-    const data = await api.listCorpusAllowedMetadata({ id: corpusId, page })
-    commit('setCorpusAllowedMetadata', { corpusId, results: data.results })
-    if (!data || !data.number || page !== data.number) {
-      // Avoid any loop
-      throw new Error(`Pagination failed listing available metadata for corpus "${corpusId}"`)
-    }
-    // Load other pages
-    if (data.next) await dispatch('listAllowedMetadata', { corpusId, page: page + 1 })
-  },
-
-  async createAllowedMetadata ({ commit }, { corpusId, ...metadata }) {
-    try {
-      const data = await api.createAllowedMetadata({ ...metadata, corpusId })
-      commit('setCorpusAllowedMetadata', { corpusId, results: [data] })
-    } catch (err) {
-      commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
-      throw err
-    }
-  },
-
-  async updateAllowedMetadata ({ commit }, { corpusId, mdId, ...metadata }) {
-    try {
-      const data = await api.updateAllowedMetadata({ corpusId, id: mdId, ...metadata })
-      commit('updateCorpusAllowedMetadata', { corpusId, data })
-    } catch (err) {
-      commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
-      throw err
-    }
-  },
-
-  async deleteAllowedMetadata ({ commit }, { corpusId, mdId }) {
-    try {
-      await api.deleteAllowedMetadata(corpusId, mdId)
-      commit('removeCorpusAllowedMetadata', { corpusId, mdId })
-    } catch (err) {
-      commit('notifications/notify', { type: 'error', text: errorParser(err) }, { root: true })
-      throw err
-    }
-  },
-
   async createType ({ commit }, { corpus, ...type }) {
     try {
       const data = await api.createType({ corpus, ...type })
diff --git a/src/stores/allowedMetadata.ts b/src/stores/allowedMetadata.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e691f51ea15eaaec82f694794cbbf98c5773280f
--- /dev/null
+++ b/src/stores/allowedMetadata.ts
@@ -0,0 +1,76 @@
+import { defineStore } from 'pinia'
+import { UUID, AllowedMetaData } from '@/types'
+import * as api from '@/api'
+import { useNotificationStore } from '.'
+import { errorParser } from '@/helpers'
+
+interface State {
+  allowedMetadata: {
+    [corpusId: UUID]: AllowedMetaData[]
+  }
+}
+
+export const useAllowedMetaDataStore = defineStore('allowedMetadata', {
+  state: (): State => ({
+    allowedMetadata: {}
+  }),
+  actions: {
+    setCorpusAllowedMetadata (corpusId: UUID, allowedMetadata: AllowedMetaData[]) {
+      const updatedList = this.allowedMetadata[corpusId] || []
+      allowedMetadata.forEach(newMeta => {
+        // Prevent duplicating allowed metadata
+        if (!updatedList.some(meta => meta.type === newMeta.type && meta.name === newMeta.name)) updatedList.push(newMeta)
+      })
+      // Merge
+      this.allowedMetadata[corpusId] = updatedList
+    },
+    async listAllowedMetadata (corpusId: UUID, page = 1) {
+      // Do not start fetching if they have already been retrieved already
+      if (page === 1 && this.allowedMetadata[corpusId]) return
+      // Automatically list all allowed metadata for a corpus through pagination
+      const data = await api.listCorpusAllowedMetadata({ id: corpusId, page })
+      this.setCorpusAllowedMetadata(corpusId, data.results)
+      if (!data || !data.number || page !== data.number) {
+        // Avoid any loop
+        throw new Error(`Pagination failed listing available metadata for corpus "${corpusId}"`)
+      }
+      // Load other page
+      if (data.next) await this.listAllowedMetadata(corpusId, page + 1)
+    },
+    async createAllowedMetadata (corpusId: UUID, metadata: Omit<AllowedMetaData, 'id'>) {
+      try {
+        const data = await api.createAllowedMetadata({ corpusId, ...metadata })
+        this.setCorpusAllowedMetadata(corpusId, [data])
+      } catch (err) {
+        useNotificationStore().notify({ type: 'error', text: errorParser(err) })
+        throw err
+      }
+    },
+    async updateAllowedMetadata (corpusId: UUID, metadata: AllowedMetaData) {
+      try {
+        const data = await api.updateAllowedMetadata(corpusId, metadata.id, metadata)
+        if (!this.allowedMetadata[corpusId]) throw new Error(`Allowed metadata for corpus ${corpusId} not found.`)
+        const mdList = [...this.allowedMetadata[corpusId]]
+        const index = mdList.findIndex(item => item.id === data.id)
+        if (index < 0) throw new Error(`Allowed metadata ${metadata.id} not found in corpus ${corpusId}`)
+        mdList.splice(index, 1, data)
+        this.allowedMetadata[corpusId] = mdList
+      } catch (err) {
+        useNotificationStore().notify({ type: 'error', text: errorParser(err) })
+        throw err
+      }
+    },
+    async deleteAllowedMetadata (corpusId: UUID, metaDataId: UUID) {
+      try {
+        await api.deleteAllowedMetadata(corpusId, metaDataId)
+        if (!this.allowedMetadata[corpusId]) throw new Error(`Allowed metadata for corpus ${corpusId} not found.`)
+        const index = this.allowedMetadata[corpusId].findIndex(item => item.id === metaDataId)
+        if (index < 0) throw new Error(`Allowed metadata ${metaDataId} not found in corpus ${corpusId}.`)
+        this.allowedMetadata[corpusId].splice(index, 1)
+      } catch (err) {
+        useNotificationStore().notify({ type: 'error', text: errorParser(err) })
+        throw err
+      }
+    }
+  }
+})
diff --git a/src/stores/index.ts b/src/stores/index.ts
index b57e903c8d6bd02d48e10a2a47effbf853a1c496..a3d50742ca97f4b8ef663acf579583cdd8133a65 100644
--- a/src/stores/index.ts
+++ b/src/stores/index.ts
@@ -16,3 +16,4 @@ export { useExportStore } from './exports'
 export { useEntityStore } from './entity'
 export { useTranscriptionStore } from './transcription'
 export { useMetaDataStore } from './metadata'
+export { useAllowedMetaDataStore } from './allowedMetadata'
diff --git a/tests/unit/filterbar.spec.js b/tests/unit/filterbar.spec.js
index 5b7ff8a9581209dc363f970572314b2eaef26c82..2daa1ab34f336ceb64a3ceddc572ba0e8b2b0abc 100644
--- a/tests/unit/filterbar.spec.js
+++ b/tests/unit/filterbar.spec.js
@@ -2,7 +2,7 @@ import { assert } from 'chai'
 import axios from 'axios'
 import store from './store/index.spec.js'
 import { createPinia, setActivePinia } from 'pinia'
-import { useMLResultsStore } from '@/stores'
+import { useMLResultsStore, useAllowedMetaDataStore } from '@/stores'
 import { FakeAxios, assertRejects } from './testhelpers.js'
 import { workerVersionsCacheSample, workerVersionsSample } from './samples.js'
 import {
@@ -22,12 +22,13 @@ import {
 } from '@/filterbar'
 
 describe('filterbar.js', () => {
-  let mock, mlResultsStore
+  let mock, mlResultsStore, allowedMetadataStore
 
   before(() => {
     setActivePinia(createPinia())
     mock = new FakeAxios(axios)
     mlResultsStore = useMLResultsStore()
+    allowedMetadataStore = useAllowedMetaDataStore()
   })
 
   beforeEach(() => {
@@ -39,6 +40,8 @@ describe('filterbar.js', () => {
 
   afterEach(() => {
     store.reset()
+    allowedMetadataStore.$reset()
+    mlResultsStore.$reset()
     mock.reset()
   })
 
@@ -963,7 +966,7 @@ describe('filterbar.js', () => {
         ])
       })
 
-      it('fetches worker versions only when needed', async () => {
+      it('fetches worker versions when needed', async () => {
         store.state.navigation.corpusId = 'corpusid'
         mlResultsStore.corpusWorkerVersionsCache = {}
         mock.onGet('/corpus/corpusid/versions/').reply(200, workerVersionsCacheSample)
@@ -1058,7 +1061,7 @@ describe('filterbar.js', () => {
         assert.strictEqual(err.toString(), "Error: Unknown worker version ID: 'the wrong version'")
       })
 
-      it('fetches worker versions only when needed', async () => {
+      it('fetches worker versions when needed', async () => {
         store.state.navigation.corpusId = 'corpusid'
         mlResultsStore.corpusWorkerVersionsCache = {}
         mock.onGet('/corpus/corpusid/versions/').reply(200, workerVersionsCacheSample)
@@ -1120,7 +1123,7 @@ describe('filterbar.js', () => {
         assert.strictEqual(await filter.deserialize('the wrong version'), undefined)
       })
 
-      it('fetches worker versions only when needed', async () => {
+      it('fetches worker versions when needed', async () => {
         store.state.navigation.corpusId = 'corpusid'
         mlResultsStore.corpusWorkerVersionsCache = {}
         mock.onGet('/corpus/corpusid/versions/').reply(200, workerVersionsCacheSample)
@@ -1145,7 +1148,7 @@ describe('filterbar.js', () => {
       assert.strictEqual(filter.store, store)
     })
 
-    const allowedMetadata = [
+    const testAllowedMetadata = [
       {
         name: 'le url',
         type: 'url'
@@ -1162,7 +1165,7 @@ describe('filterbar.js', () => {
       })
 
       it('autocompletes with allowed metadata', async () => {
-        store.state.corpora.corpusAllowedMetadata.corpusid = allowedMetadata
+        allowedMetadataStore.allowedMetadata.corpusid = testAllowedMetadata
 
         const filter = new MetadataNameFilter(store)
 
@@ -1176,7 +1179,7 @@ describe('filterbar.js', () => {
         mock.onGet('/corpus/corpusid/allowed-metadata/').reply(200, {
           count: 2,
           number: 1,
-          results: allowedMetadata
+          results: testAllowedMetadata
         })
 
         const filter = new MetadataNameFilter(store)
@@ -1186,26 +1189,14 @@ describe('filterbar.js', () => {
           'le number'
         ])
 
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/listAllowedMetadata',
-            payload: {
-              corpusId: 'corpusid'
-            }
-          },
-          {
-            mutation: 'corpora/setCorpusAllowedMetadata',
-            payload: {
-              corpusId: 'corpusid',
-              results: allowedMetadata
-            }
-          }
-        ])
+        assert.deepStrictEqual(allowedMetadataStore.allowedMetadata, {
+          corpusid: testAllowedMetadata
+        })
       })
     })
 
     it('allows any name', () => {
-      store.state.corpora.corpusAllowedMetadata.corpusid = allowedMetadata
+      allowedMetadataStore.allowedMetadata.corpusid = testAllowedMetadata
 
       const filter = new MetadataNameFilter(store)
 
diff --git a/tests/unit/store/corpora.spec.js b/tests/unit/store/corpora.spec.js
index 6e2252d5d25df43c483c7381205eb2b25eee4877..03c14e6391cdbca68f242f3d88a7f2d934b81374 100644
--- a/tests/unit/store/corpora.spec.js
+++ b/tests/unit/store/corpora.spec.js
@@ -310,162 +310,6 @@ describe('corpora', () => {
       })
     })
 
-    describe('setCorpusAllowedMetadata', () => {
-      it('updates allowed metadata on a corpus', () => {
-        const state = {
-          corpusAllowedMetadata: {
-            corpus1: [
-              { type: 'A', name: 'A' },
-              { type: 'B', name: 'B' }
-            ]
-          }
-        }
-        const addedAllowedMeta = [
-          { type: 'A', name: 'A' },
-          { type: 'A', name: 'B' },
-          { type: 'B', name: 'A' }
-        ]
-
-        mutations.setCorpusAllowedMetadata(state, { corpusId: 'corpus1', results: addedAllowedMeta })
-
-        assert.deepStrictEqual(state, {
-          corpusAllowedMetadata: {
-            corpus1: [
-              { type: 'A', name: 'A' },
-              { type: 'B', name: 'B' },
-              { type: 'A', name: 'B' },
-              { type: 'B', name: 'A' }
-            ]
-          }
-        })
-      })
-    })
-
-    describe('updateCorpusAllowedMetadata', () => {
-      it('updates an existing allowed metadata (name, type) couple in state.store.corpusAllowedMetadata[corpusId]', async () => {
-        const state = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'twoId', name: 'twoName', type: 'twoType' },
-              { id: 'threeId', name: 'threeName', type: 'threeType' }
-            ]
-          }
-        }
-        const expected = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'twoId', name: 'twoName', type: 'twoType' },
-              { id: 'threeId', name: 'fourName', type: 'threeType' }
-            ]
-          }
-        }
-        mutations.updateCorpusAllowedMetadata(state, { corpusId: 'someCorpus', data: { id: 'threeId', type: 'threeType', name: 'fourName' } })
-        assert.deepStrictEqual(state, expected)
-      })
-      it('throws an error if the metadata id is not found in the corpus allowed metadata', async () => {
-        const state = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'twoId', name: 'twoName', type: 'twoType' },
-              { id: 'threeId', name: 'threeName', type: 'threeType' }
-            ]
-          }
-        }
-        assert.throws(
-          () => mutations.updateCorpusAllowedMetadata(state, { corpusId: 'someCorpus', data: { id: 'missingId', name: 'someName', type: 'someType' } }),
-          'Allowed metadata missingId not found in corpus someCorpus'
-        )
-      })
-      it('throws an error if there is no state.store.corpusAllowedMetadata[corpusId]', async () => {
-        const state = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {}
-        }
-        assert.throws(
-          () => mutations.updateCorpusAllowedMetadata(state, { corpusId: 'someCorpus', mdId: 'missingId' }),
-          'Allowed metadata for corpus someCorpus not found'
-        )
-      })
-    })
-
-    describe('removeCorpusAllowedMetadata', () => {
-      it('removes an allowed metadata from state.store.corpusAllowedMetadata[corpusId]', async () => {
-        const state = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'twoId', name: 'twoName', type: 'twoType' },
-              { id: 'threeId', name: 'threeName', type: 'threeType' }
-            ]
-          }
-        }
-        const expected = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'threeId', name: 'threeName', type: 'threeType' }
-            ]
-          }
-        }
-        mutations.removeCorpusAllowedMetadata(state, { corpusId: 'someCorpus', mdId: 'twoId' })
-        assert.deepStrictEqual(state, expected)
-      })
-      it('throws an error if the metadata id is not found in the corpus allowed metadata', async () => {
-        const state = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'twoId', name: 'twoName', type: 'twoType' },
-              { id: 'threeId', name: 'threeName', type: 'threeType' }
-            ]
-          }
-        }
-        const expected = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {
-            someCorpus: [
-              { id: 'oneId', name: 'oneName', type: 'oneType' },
-              { id: 'twoId', name: 'twoName', type: 'twoType' },
-              { id: 'threeId', name: 'threeName', type: 'threeType' }
-            ]
-          }
-        }
-        assert.throws(
-          () => mutations.removeCorpusAllowedMetadata(state, { corpusId: 'someCorpus', mdId: 'missingId' }),
-          'Allowed metadata missingId not found in corpus someCorpus'
-        )
-        assert.deepStrictEqual(state, expected)
-      })
-      it('handles errors', async () => {
-        const state = {
-          corpora: {},
-          corporaLoaded: false,
-          corpusAllowedMetadata: {}
-        }
-        assert.throws(
-          () => mutations.removeCorpusAllowedMetadata(state, { corpusId: 'someCorpus', mdId: 'oneId' }),
-          'Allowed metadata for corpus someCorpus not found'
-        )
-      })
-    })
-
     describe('setCorpusEntityTypes', () => {
       it('updates entity types on a corpus', () => {
         const state = {
@@ -1428,194 +1272,5 @@ describe('corpora', () => {
         ])
       })
     })
-
-    describe('listAllowedMetadata', () => {
-      it('recursively lists allowed metadata for a corpus', async () => {
-        const pages = [{
-          count: 3,
-          number: 1,
-          next: 'nexpage',
-          results: [{ type: 'A', name: 'A' }, { type: 'A', name: 'B' }]
-        }, {
-          count: 3,
-          number: 2,
-          next: null,
-          results: [{ type: 'B', name: 'B' }]
-        }]
-        mock.onGet('/corpus/corpusid/allowed-metadata/', { params: { page: 1 } }).reply(200, pages[0])
-        mock.onGet('/corpus/corpusid/allowed-metadata/', { params: { page: 2 } }).reply(200, pages[1])
-
-        await store.dispatch('corpora/listAllowedMetadata', { corpusId: 'corpusid' })
-
-        assert.deepStrictEqual(store.history, [
-          { action: 'corpora/listAllowedMetadata', payload: { corpusId: 'corpusid' } },
-          { mutation: 'corpora/setCorpusAllowedMetadata', payload: { corpusId: 'corpusid', results: pages[0].results } },
-          { action: 'corpora/listAllowedMetadata', payload: { corpusId: 'corpusid', page: 2 } },
-          { mutation: 'corpora/setCorpusAllowedMetadata', payload: { corpusId: 'corpusid', results: pages[1].results } }
-        ])
-
-        assert.deepStrictEqual(store.state.corpora.corpusAllowedMetadata, {
-          corpusid: [
-            { name: 'A', type: 'A' },
-            { name: 'B', type: 'A' },
-            { name: 'B', type: 'B' }
-          ]
-        })
-      })
-    })
-
-    describe('createAllowedMetadata', () => {
-      it('creates a new allowed metadata in a corpus', async () => {
-        store.state.corpora.corpusAllowedMetadata = {
-          someCorpus: [
-            { id: 'oneId', name: 'oneName', type: 'oneType' }
-          ]
-        }
-        const response = {
-          id: 'metaId',
-          type: 'someType',
-          name: 'someName'
-        }
-        mock.onPost('/corpus/someCorpus/allowed-metadata/').reply(201, response)
-
-        await store.dispatch('corpora/createAllowedMetadata', { corpusId: 'someCorpus', type: 'someType', name: 'someName' })
-
-        assert.deepStrictEqual(store.state.corpora.corpusAllowedMetadata, {
-          someCorpus: [
-            { id: 'oneId', name: 'oneName', type: 'oneType' },
-            { id: 'metaId', name: 'someName', type: 'someType' }
-          ]
-        })
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/createAllowedMetadata',
-            payload: { corpusId: 'someCorpus', type: 'someType', name: 'someName' }
-          },
-          {
-            mutation: 'corpora/setCorpusAllowedMetadata',
-            payload: { corpusId: 'someCorpus', results: [{ id: 'metaId', type: 'someType', name: 'someName' }] }
-          }
-        ])
-      })
-
-      it('handles errors', async () => {
-        mock.onPost('/corpus/someCorpus/allowed-metadata/').reply(400)
-        await assertRejects(async () => store.dispatch('corpora/createAllowedMetadata', { corpusId: 'someCorpus', type: 'someType', name: 'someName' }))
-
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/createAllowedMetadata',
-            payload: { corpusId: 'someCorpus', type: 'someType', name: 'someName' }
-          },
-          {
-            mutation: 'notifications/notify',
-            payload: {
-              type: 'error',
-              text: 'Request failed with status code 400'
-            }
-          }
-        ])
-      })
-    })
-
-    describe('deleteAllowedMetadata', () => {
-      it('removes an allowed metadata (name, type) couple from a corpus', async () => {
-        store.state.corpora.corpusAllowedMetadata = {
-          someCorpus: [
-            { id: 'oneId', name: 'oneName', type: 'oneType' },
-            { id: 'twoId', name: 'twoName', type: 'twoType' },
-            { id: 'threeId', name: 'threeName', type: 'threeType' }
-          ]
-        }
-        mock.onDelete('/corpus/someCorpus/allowed-metadata/twoId/').reply(204)
-        await store.dispatch('corpora/deleteAllowedMetadata', { corpusId: 'someCorpus', mdId: 'twoId' })
-        assert.deepStrictEqual(store.state.corpora.corpusAllowedMetadata, {
-          someCorpus: [
-            { id: 'oneId', name: 'oneName', type: 'oneType' },
-            { id: 'threeId', name: 'threeName', type: 'threeType' }
-          ]
-        })
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/deleteAllowedMetadata',
-            payload: { corpusId: 'someCorpus', mdId: 'twoId' }
-          },
-          {
-            mutation: 'corpora/removeCorpusAllowedMetadata',
-            payload: { corpusId: 'someCorpus', mdId: 'twoId' }
-          }
-        ])
-      })
-      it('handles errors', async () => {
-        mock.onDelete('/corpus/someCorpus/allowed-metadata/missingId/').reply(400)
-        await assertRejects(async () => await store.dispatch('corpora/deleteAllowedMetadata', { corpusId: 'someCorpus', mdId: 'missingId' }))
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/deleteAllowedMetadata',
-            payload: { corpusId: 'someCorpus', mdId: 'missingId' }
-          },
-          {
-            mutation: 'notifications/notify',
-            payload: {
-              type: 'error',
-              text: 'Request failed with status code 400'
-            }
-          }
-        ])
-      })
-    })
-
-    describe('updateAllowedMetadata', () => {
-      it('updates an existing allowed metadata (name, type) couple in a corpus', async () => {
-        store.state.corpora.corpusAllowedMetadata = {
-          someCorpus: [
-            { id: 'oneId', name: 'oneName', type: 'oneType' },
-            { id: 'twoId', name: 'twoName', type: 'twoType' },
-            { id: 'threeId', name: 'threeName', type: 'threeType' }
-          ]
-        }
-        const response = {
-          id: 'threeId',
-          type: 'fourType',
-          name: 'threeName'
-        }
-        mock.onPatch('/corpus/someCorpus/allowed-metadata/threeId/').reply(200, response)
-        await store.dispatch('corpora/updateAllowedMetadata', { corpusId: 'someCorpus', mdId: 'threeId', type: 'fourType', name: 'threeName' })
-        assert.deepStrictEqual(store.state.corpora.corpusAllowedMetadata, {
-          someCorpus: [
-            { id: 'oneId', name: 'oneName', type: 'oneType' },
-            { id: 'twoId', name: 'twoName', type: 'twoType' },
-            { id: 'threeId', name: 'threeName', type: 'fourType' }
-          ]
-        })
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/updateAllowedMetadata',
-            payload: { corpusId: 'someCorpus', mdId: 'threeId', type: 'fourType', name: 'threeName' }
-          },
-          {
-            mutation: 'corpora/updateCorpusAllowedMetadata',
-            payload: { corpusId: 'someCorpus', data: { id: 'threeId', type: 'fourType', name: 'threeName' } }
-          }
-        ])
-      })
-      it('handles errors', async () => {
-        mock.onPatch('/corpus/someCorpus/allowed-metadata/missingId/').reply(400)
-        await assertRejects(async () => await store.dispatch('corpora/updateAllowedMetadata', { corpusId: 'someCorpus', mdId: 'missingId', name: 'someName', type: 'someType' }))
-        assert.deepStrictEqual(store.history, [
-          {
-            action: 'corpora/updateAllowedMetadata',
-            payload: { corpusId: 'someCorpus', mdId: 'missingId', name: 'someName', type: 'someType' }
-          },
-          {
-            mutation: 'notifications/notify',
-            payload: {
-              type: 'error',
-              text: 'Request failed with status code 400'
-            }
-          }
-        ])
-      })
-    })
   })
 })
diff --git a/tests/unit/stores/allowedMetadata.spec.js b/tests/unit/stores/allowedMetadata.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f30c365750feea4a7aef4e8a633808b8dbab8a17
--- /dev/null
+++ b/tests/unit/stores/allowedMetadata.spec.js
@@ -0,0 +1,169 @@
+import { assert } from 'chai'
+import axios from 'axios'
+import { useAllowedMetaDataStore, useNotificationStore } from '@/stores'
+import { assertRejects, FakeAxios } from '../testhelpers'
+import { createPinia, setActivePinia } from 'pinia'
+
+describe('allowedMetadata', () => {
+  describe('actions', () => {
+    let mock, store, notificationStore
+
+    before('Setting up mocks', () => {
+      mock = new FakeAxios(axios)
+      setActivePinia(createPinia())
+      store = useAllowedMetaDataStore()
+      notificationStore = useNotificationStore()
+    })
+
+    afterEach(() => {
+      // Remove any handlers, but leave mocking in place
+      mock.reset()
+      store.$reset()
+      notificationStore.$reset()
+    })
+
+    after('Removing Axios mock', () => {
+      // Remove mocking entirely
+      mock.restore()
+    })
+
+    describe('setCorpusAllowedMetadata', () => {
+      it('updates allowed metadata on a corpus', () => {
+        store.allowedMetadata = {
+          corpus1: [
+            { type: 'A', name: 'A' },
+            { type: 'B', name: 'B' }
+          ]
+        }
+        const addedAllowedMeta = [
+          { type: 'A', name: 'A' },
+          { type: 'A', name: 'B' },
+          { type: 'B', name: 'A' }
+        ]
+
+        store.setCorpusAllowedMetadata('corpus1', addedAllowedMeta)
+
+        assert.deepStrictEqual(store.allowedMetadata, {
+          corpus1: [
+            { type: 'A', name: 'A' },
+            { type: 'B', name: 'B' },
+            { type: 'A', name: 'B' },
+            { type: 'B', name: 'A' }
+          ]
+        })
+      })
+    })
+
+    describe('listAllowedMetadata', () => {
+      it('recursively lists allowed metadata for a corpus', async () => {
+        const pages = [{
+          count: 3,
+          number: 1,
+          next: 'nexpage',
+          results: [{ type: 'A', name: 'A' }, { type: 'A', name: 'B' }]
+        }, {
+          count: 3,
+          number: 2,
+          next: null,
+          results: [{ type: 'B', name: 'B' }]
+        }]
+        mock.onGet('/corpus/corpusid/allowed-metadata/', { params: { page: 1 } }).reply(200, pages[0])
+        mock.onGet('/corpus/corpusid/allowed-metadata/', { params: { page: 2 } }).reply(200, pages[1])
+
+        await store.listAllowedMetadata('corpusid')
+
+        assert.deepStrictEqual(store.allowedMetadata, {
+          corpusid: [
+            { name: 'A', type: 'A' },
+            { name: 'B', type: 'A' },
+            { name: 'B', type: 'B' }
+          ]
+        })
+      })
+    })
+
+    describe('createAllowedMetadata', () => {
+      it('creates a new allowed metadata in a corpus', async () => {
+        store.allowedMetadata = {
+          someCorpus: [
+            { id: 'oneId', name: 'oneName', type: 'oneType' }
+          ]
+        }
+        const response = {
+          id: 'metaId',
+          type: 'someType',
+          name: 'someName'
+        }
+        mock.onPost('/corpus/someCorpus/allowed-metadata/').reply(201, response)
+
+        await store.createAllowedMetadata('someCorpus', { type: 'someType', name: 'someName' })
+
+        assert.deepStrictEqual(store.allowedMetadata, {
+          someCorpus: [
+            { id: 'oneId', name: 'oneName', type: 'oneType' },
+            { id: 'metaId', name: 'someName', type: 'someType' }
+          ]
+        })
+      })
+
+      it('handles errors', async () => {
+        mock.onPost('/corpus/someCorpus/allowed-metadata/').reply(400)
+        await assertRejects(async () => store.createAllowedMetadata('someCorpus', { type: 'someType', name: 'someName' }))
+      })
+    })
+
+    describe('deleteAllowedMetadata', () => {
+      it('removes an allowed metadata (name, type) couple from a corpus', async () => {
+        store.allowedMetadata = {
+          someCorpus: [
+            { id: 'oneId', name: 'oneName', type: 'oneType' },
+            { id: 'twoId', name: 'twoName', type: 'twoType' },
+            { id: 'threeId', name: 'threeName', type: 'threeType' }
+          ]
+        }
+        mock.onDelete('/corpus/someCorpus/allowed-metadata/twoId/').reply(204)
+        await store.deleteAllowedMetadata('someCorpus', 'twoId')
+        assert.deepStrictEqual(store.allowedMetadata, {
+          someCorpus: [
+            { id: 'oneId', name: 'oneName', type: 'oneType' },
+            { id: 'threeId', name: 'threeName', type: 'threeType' }
+          ]
+        })
+      })
+      it('handles errors', async () => {
+        mock.onDelete('/corpus/someCorpus/allowed-metadata/missingId/').reply(400)
+        await assertRejects(async () => await store.deleteAllowedMetadata('someCorpus', 'missingId'))
+      })
+    })
+
+    describe('updateAllowedMetadata', () => {
+      it('updates an existing allowed metadata (name, type) couple in a corpus', async () => {
+        store.allowedMetadata = {
+          someCorpus: [
+            { id: 'oneId', name: 'oneName', type: 'oneType' },
+            { id: 'twoId', name: 'twoName', type: 'twoType' },
+            { id: 'threeId', name: 'threeName', type: 'threeType' }
+          ]
+        }
+        const response = {
+          id: 'threeId',
+          type: 'fourType',
+          name: 'threeName'
+        }
+        mock.onPatch('/corpus/someCorpus/allowed-metadata/threeId/').reply(200, response)
+        await store.updateAllowedMetadata('someCorpus', { id: 'threeId', type: 'fourType', name: 'threeName' })
+        assert.deepStrictEqual(store.allowedMetadata, {
+          someCorpus: [
+            { id: 'oneId', name: 'oneName', type: 'oneType' },
+            { id: 'twoId', name: 'twoName', type: 'twoType' },
+            { id: 'threeId', name: 'threeName', type: 'fourType' }
+          ]
+        })
+      })
+      it('handles errors', async () => {
+        mock.onPatch('/corpus/someCorpus/allowed-metadata/missingId/').reply(400)
+        await assertRejects(async () => await store.updateAllowedMetadata('someCorpus', { id: 'missingId', name: 'someName', type: 'someType' }))
+      })
+    })
+  })
+})