diff --git a/.dev-js/.eslintrc.js b/.dev-js/.eslintrc.js
new file mode 100644
index 0000000000000000000000000000000000000000..40571d02f4c429d79d9ec85a4f37ba8c78f74b4c
--- /dev/null
+++ b/.dev-js/.eslintrc.js
@@ -0,0 +1,252 @@
+module.exports = {
+  root: true,
+  overrides: [
+    {
+      files: ["*.js", "*.vue"],
+      // parser: "vue-eslint-parser",
+      //processor: "@graphql-eslint/graphql",
+      extends: [
+        "eslint:recommended",
+        "plugin:vue/strongly-recommended",
+        "plugin:@intlify/vue-i18n/recommended",
+      ],
+      rules: {
+        "no-unused-vars": "warn",
+        "vue/no-unused-vars": "off",
+        "vue/multi-word-component-names": "off",
+        "vue/attribute-hyphenation": "error",
+        "vue/v-slot-style": "error",
+        "@intlify/vue-i18n/key-format-style": [
+          "error",
+          "snake_case",
+          {
+            splitByDots: false,
+          },
+        ],
+        // "@intlify/vue-i18n/no-unused-keys": ["warn", {}],
+        "@intlify/vue-i18n/no-raw-text": [
+          "error",
+          {
+            ignoreNodes: ["v-icon"],
+            ignorePattern: "^[-–—·#:()\\[\\]&\\.\\s]+$",
+          },
+        ],
+        "@intlify/vue-i18n/no-deprecated-tc": "off",
+        // Fixes for prettier (avoid eslint-config-prettier)
+        // The following rules can be used in some cases. See the README for more
+        // information. (These are marked with `0` instead of `"off"` so that a
+        // script can distinguish them.)
+        curly: 0,
+        "lines-around-comment": 0,
+        "max-len": 0,
+        "no-confusing-arrow": 0,
+        "no-mixed-operators": 0,
+        "no-tabs": 0,
+        "no-unexpected-multiline": 0,
+        quotes: 0,
+        "@typescript-eslint/quotes": 0,
+        "babel/quotes": 0,
+        "vue/html-self-closing": 0,
+        "vue/max-len": 0,
+
+        // The rest are rules that you never need to enable when using Prettier.
+        "array-bracket-newline": "off",
+        "array-bracket-spacing": "off",
+        "array-element-newline": "off",
+        "arrow-parens": "off",
+        "arrow-spacing": "off",
+        "block-spacing": "off",
+        "brace-style": "off",
+        "comma-dangle": "off",
+        "comma-spacing": "off",
+        "comma-style": "off",
+        "computed-property-spacing": "off",
+        "dot-location": "off",
+        "eol-last": "off",
+        "func-call-spacing": "off",
+        "function-call-argument-newline": "off",
+        "function-paren-newline": "off",
+        "generator-star": "off",
+        "generator-star-spacing": "off",
+        "implicit-arrow-linebreak": "off",
+        indent: "off",
+        "jsx-quotes": "off",
+        "key-spacing": "off",
+        "keyword-spacing": "off",
+        "linebreak-style": "off",
+        "multiline-ternary": "off",
+        "newline-per-chained-call": "off",
+        "new-parens": "off",
+        "no-arrow-condition": "off",
+        "no-comma-dangle": "off",
+        "no-extra-parens": "off",
+        "no-extra-semi": "off",
+        "no-floating-decimal": "off",
+        "no-mixed-spaces-and-tabs": "off",
+        "no-multi-spaces": "off",
+        "no-multiple-empty-lines": "off",
+        "no-reserved-keys": "off",
+        "no-space-before-semi": "off",
+        "no-trailing-spaces": "off",
+        "no-whitespace-before-property": "off",
+        "no-wrap-func": "off",
+        "nonblock-statement-body-position": "off",
+        "object-curly-newline": "off",
+        "object-curly-spacing": "off",
+        "object-property-newline": "off",
+        "one-var-declaration-per-line": "off",
+        "operator-linebreak": "off",
+        "padded-blocks": "off",
+        "quote-props": "off",
+        "rest-spread-spacing": "off",
+        semi: "off",
+        "semi-spacing": "off",
+        "semi-style": "off",
+        "space-after-function-name": "off",
+        "space-after-keywords": "off",
+        "space-before-blocks": "off",
+        "space-before-function-paren": "off",
+        "space-before-function-parentheses": "off",
+        "space-before-keywords": "off",
+        "space-in-brackets": "off",
+        "space-in-parens": "off",
+        "space-infix-ops": "off",
+        "space-return-throw-case": "off",
+        "space-unary-ops": "off",
+        "space-unary-word-ops": "off",
+        "switch-colon-spacing": "off",
+        "template-curly-spacing": "off",
+        "template-tag-spacing": "off",
+        "unicode-bom": "off",
+        "wrap-iife": "off",
+        "wrap-regex": "off",
+        "yield-star-spacing": "off",
+        "@babel/object-curly-spacing": "off",
+        "@babel/semi": "off",
+        "@typescript-eslint/brace-style": "off",
+        "@typescript-eslint/comma-dangle": "off",
+        "@typescript-eslint/comma-spacing": "off",
+        "@typescript-eslint/func-call-spacing": "off",
+        "@typescript-eslint/indent": "off",
+        "@typescript-eslint/keyword-spacing": "off",
+        "@typescript-eslint/member-delimiter-style": "off",
+        "@typescript-eslint/no-extra-parens": "off",
+        "@typescript-eslint/no-extra-semi": "off",
+        "@typescript-eslint/object-curly-spacing": "off",
+        "@typescript-eslint/semi": "off",
+        "@typescript-eslint/space-before-blocks": "off",
+        "@typescript-eslint/space-before-function-paren": "off",
+        "@typescript-eslint/space-infix-ops": "off",
+        "@typescript-eslint/type-annotation-spacing": "off",
+        "babel/object-curly-spacing": "off",
+        "babel/semi": "off",
+        "flowtype/boolean-style": "off",
+        "flowtype/delimiter-dangle": "off",
+        "flowtype/generic-spacing": "off",
+        "flowtype/object-type-curly-spacing": "off",
+        "flowtype/object-type-delimiter": "off",
+        "flowtype/quotes": "off",
+        "flowtype/semi": "off",
+        "flowtype/space-after-type-colon": "off",
+        "flowtype/space-before-generic-bracket": "off",
+        "flowtype/space-before-type-colon": "off",
+        "flowtype/union-intersection-spacing": "off",
+        "react/jsx-child-element-spacing": "off",
+        "react/jsx-closing-bracket-location": "off",
+        "react/jsx-closing-tag-location": "off",
+        "react/jsx-curly-newline": "off",
+        "react/jsx-curly-spacing": "off",
+        "react/jsx-equals-spacing": "off",
+        "react/jsx-first-prop-new-line": "off",
+        "react/jsx-indent": "off",
+        "react/jsx-indent-props": "off",
+        "react/jsx-max-props-per-line": "off",
+        "react/jsx-newline": "off",
+        "react/jsx-one-expression-per-line": "off",
+        "react/jsx-props-no-multi-spaces": "off",
+        "react/jsx-tag-spacing": "off",
+        "react/jsx-wrap-multilines": "off",
+        "standard/array-bracket-even-spacing": "off",
+        "standard/computed-property-even-spacing": "off",
+        "standard/object-curly-even-spacing": "off",
+        "unicorn/empty-brace-spaces": "off",
+        "unicorn/no-nested-ternary": "off",
+        "unicorn/number-literal-case": "off",
+        "vue/array-bracket-newline": "off",
+        "vue/array-bracket-spacing": "off",
+        "vue/arrow-spacing": "off",
+        "vue/block-spacing": "off",
+        "vue/block-tag-newline": "off",
+        "vue/brace-style": "off",
+        "vue/comma-dangle": "off",
+        "vue/comma-spacing": "off",
+        "vue/comma-style": "off",
+        "vue/dot-location": "off",
+        "vue/func-call-spacing": "off",
+        "vue/html-closing-bracket-newline": "off",
+        "vue/html-closing-bracket-spacing": "off",
+        "vue/html-end-tags": "off",
+        "vue/html-indent": "off",
+        "vue/html-quotes": "off",
+        "vue/key-spacing": "off",
+        "vue/keyword-spacing": "off",
+        "vue/max-attributes-per-line": "off",
+        "vue/multiline-html-element-content-newline": "off",
+        "vue/multiline-ternary": "off",
+        "vue/mustache-interpolation-spacing": "off",
+        "vue/no-extra-parens": "off",
+        "vue/no-multi-spaces": "off",
+        "vue/no-spaces-around-equal-signs-in-attribute": "off",
+        "vue/object-curly-newline": "off",
+        "vue/object-curly-spacing": "off",
+        "vue/object-property-newline": "off",
+        "vue/operator-linebreak": "off",
+        "vue/quote-props": "off",
+        "vue/script-indent": "off",
+        "vue/singleline-html-element-content-newline": "off",
+        "vue/space-in-parens": "off",
+        "vue/space-infix-ops": "off",
+        "vue/space-unary-ops": "off",
+        "vue/template-curly-spacing": "off",
+      },
+      settings: {
+        "vue-i18n": {
+          localeDir: "./aleksis/core/frontend/messages/*.{json}",
+          messageSyntaxVersion: "^8.0.0",
+        },
+      },
+      env: {
+        es2021: true,
+      },
+      parserOptions: {
+        ecmaVersion: "latest",
+      },
+    },
+    {
+      files: ["*.graphql"],
+      parser: "@graphql-eslint/eslint-plugin",
+      plugins: ["@graphql-eslint"],
+      extends: "plugin:@graphql-eslint/operations-recommended",
+      parserOptions: {
+        graphQLConfig: {
+          schema: "./schema.json",
+          documents: "../aleksis/**/*/frontend/**/*.graphql",
+        },
+      },
+      rules: {
+        "@graphql-eslint/no-anonymous-operations": "error",
+        "@graphql-eslint/no-duplicate-fields": "error",
+        "@graphql-eslint/naming-convention": [
+          "error",
+          {
+            OperationDefinition: {
+              style: "camelCase",
+              forbiddenPrefixes: ["Query", "Mutation", "Subscription", "Get"],
+              forbiddenSuffixes: ["Query", "Mutation", "Subscription"],
+            },
+          },
+        ],
+      },
+    },
+  ],
+};
diff --git a/.dev-js/package.json b/.dev-js/package.json
index baf299f86c8dedc316ac23108629472442053dd9..298e6c130173b7870174169d5968bf58e2a41050 100644
--- a/.dev-js/package.json
+++ b/.dev-js/package.json
@@ -2,10 +2,12 @@
   "name": "aleksis-builddeps",
   "version": "1.0.0",
   "dependencies": {
+    "@graphql-eslint/eslint-plugin": "^4.3.0",
     "@intlify/eslint-plugin-vue-i18n": "^3.0.0",
     "eslint": "^8.26.0",
     "eslint-config-prettier": "^9.0.0",
     "eslint-plugin-vue": "^9.7.0",
+    "graphql": "^16.10.0",
     "prettier": "^3.4.0",
     "stylelint": "^16.0.0",
     "stylelint-config-prettier": "^9.0.3",
diff --git a/.eslintrc.js b/.eslintrc.js
deleted file mode 100644
index 60317f4987d885fc380d731c6dd8792325342f44..0000000000000000000000000000000000000000
--- a/.eslintrc.js
+++ /dev/null
@@ -1,215 +0,0 @@
-module.exports = {
-  extends: [
-    "eslint:recommended",
-    "plugin:vue/strongly-recommended",
-    "plugin:@intlify/vue-i18n/recommended",
-  ],
-  rules: {
-    "no-unused-vars": "warn",
-    "vue/no-unused-vars": "off",
-    "vue/multi-word-component-names": "off",
-    "@intlify/vue-i18n/key-format-style": [
-      "error",
-      "snake_case",
-      {
-        splitByDots: false,
-      },
-    ],
-    // "@intlify/vue-i18n/no-unused-keys": ["warn", {}],
-    "@intlify/vue-i18n/no-raw-text": [
-      "error",
-      {
-        ignoreNodes: ["v-icon"],
-        ignorePattern: "^[-–—·#:()\\[\\]&\\.\\s]+$",
-      },
-    ],
-    // Fixes for prettier (avoid eslint-config-prettier)
-    // The following rules can be used in some cases. See the README for more
-    // information. (These are marked with `0` instead of `"off"` so that a
-    // script can distinguish them.)
-    curly: 0,
-    "lines-around-comment": 0,
-    "max-len": 0,
-    "no-confusing-arrow": 0,
-    "no-mixed-operators": 0,
-    "no-tabs": 0,
-    "no-unexpected-multiline": 0,
-    quotes: 0,
-    "@typescript-eslint/quotes": 0,
-    "babel/quotes": 0,
-    "vue/html-self-closing": 0,
-    "vue/max-len": 0,
-
-    // The rest are rules that you never need to enable when using Prettier.
-    "array-bracket-newline": "off",
-    "array-bracket-spacing": "off",
-    "array-element-newline": "off",
-    "arrow-parens": "off",
-    "arrow-spacing": "off",
-    "block-spacing": "off",
-    "brace-style": "off",
-    "comma-dangle": "off",
-    "comma-spacing": "off",
-    "comma-style": "off",
-    "computed-property-spacing": "off",
-    "dot-location": "off",
-    "eol-last": "off",
-    "func-call-spacing": "off",
-    "function-call-argument-newline": "off",
-    "function-paren-newline": "off",
-    "generator-star": "off",
-    "generator-star-spacing": "off",
-    "implicit-arrow-linebreak": "off",
-    indent: "off",
-    "jsx-quotes": "off",
-    "key-spacing": "off",
-    "keyword-spacing": "off",
-    "linebreak-style": "off",
-    "multiline-ternary": "off",
-    "newline-per-chained-call": "off",
-    "new-parens": "off",
-    "no-arrow-condition": "off",
-    "no-comma-dangle": "off",
-    "no-extra-parens": "off",
-    "no-extra-semi": "off",
-    "no-floating-decimal": "off",
-    "no-mixed-spaces-and-tabs": "off",
-    "no-multi-spaces": "off",
-    "no-multiple-empty-lines": "off",
-    "no-reserved-keys": "off",
-    "no-space-before-semi": "off",
-    "no-trailing-spaces": "off",
-    "no-whitespace-before-property": "off",
-    "no-wrap-func": "off",
-    "nonblock-statement-body-position": "off",
-    "object-curly-newline": "off",
-    "object-curly-spacing": "off",
-    "object-property-newline": "off",
-    "one-var-declaration-per-line": "off",
-    "operator-linebreak": "off",
-    "padded-blocks": "off",
-    "quote-props": "off",
-    "rest-spread-spacing": "off",
-    semi: "off",
-    "semi-spacing": "off",
-    "semi-style": "off",
-    "space-after-function-name": "off",
-    "space-after-keywords": "off",
-    "space-before-blocks": "off",
-    "space-before-function-paren": "off",
-    "space-before-function-parentheses": "off",
-    "space-before-keywords": "off",
-    "space-in-brackets": "off",
-    "space-in-parens": "off",
-    "space-infix-ops": "off",
-    "space-return-throw-case": "off",
-    "space-unary-ops": "off",
-    "space-unary-word-ops": "off",
-    "switch-colon-spacing": "off",
-    "template-curly-spacing": "off",
-    "template-tag-spacing": "off",
-    "unicode-bom": "off",
-    "wrap-iife": "off",
-    "wrap-regex": "off",
-    "yield-star-spacing": "off",
-    "@babel/object-curly-spacing": "off",
-    "@babel/semi": "off",
-    "@typescript-eslint/brace-style": "off",
-    "@typescript-eslint/comma-dangle": "off",
-    "@typescript-eslint/comma-spacing": "off",
-    "@typescript-eslint/func-call-spacing": "off",
-    "@typescript-eslint/indent": "off",
-    "@typescript-eslint/keyword-spacing": "off",
-    "@typescript-eslint/member-delimiter-style": "off",
-    "@typescript-eslint/no-extra-parens": "off",
-    "@typescript-eslint/no-extra-semi": "off",
-    "@typescript-eslint/object-curly-spacing": "off",
-    "@typescript-eslint/semi": "off",
-    "@typescript-eslint/space-before-blocks": "off",
-    "@typescript-eslint/space-before-function-paren": "off",
-    "@typescript-eslint/space-infix-ops": "off",
-    "@typescript-eslint/type-annotation-spacing": "off",
-    "babel/object-curly-spacing": "off",
-    "babel/semi": "off",
-    "flowtype/boolean-style": "off",
-    "flowtype/delimiter-dangle": "off",
-    "flowtype/generic-spacing": "off",
-    "flowtype/object-type-curly-spacing": "off",
-    "flowtype/object-type-delimiter": "off",
-    "flowtype/quotes": "off",
-    "flowtype/semi": "off",
-    "flowtype/space-after-type-colon": "off",
-    "flowtype/space-before-generic-bracket": "off",
-    "flowtype/space-before-type-colon": "off",
-    "flowtype/union-intersection-spacing": "off",
-    "react/jsx-child-element-spacing": "off",
-    "react/jsx-closing-bracket-location": "off",
-    "react/jsx-closing-tag-location": "off",
-    "react/jsx-curly-newline": "off",
-    "react/jsx-curly-spacing": "off",
-    "react/jsx-equals-spacing": "off",
-    "react/jsx-first-prop-new-line": "off",
-    "react/jsx-indent": "off",
-    "react/jsx-indent-props": "off",
-    "react/jsx-max-props-per-line": "off",
-    "react/jsx-newline": "off",
-    "react/jsx-one-expression-per-line": "off",
-    "react/jsx-props-no-multi-spaces": "off",
-    "react/jsx-tag-spacing": "off",
-    "react/jsx-wrap-multilines": "off",
-    "standard/array-bracket-even-spacing": "off",
-    "standard/computed-property-even-spacing": "off",
-    "standard/object-curly-even-spacing": "off",
-    "unicorn/empty-brace-spaces": "off",
-    "unicorn/no-nested-ternary": "off",
-    "unicorn/number-literal-case": "off",
-    "vue/array-bracket-newline": "off",
-    "vue/array-bracket-spacing": "off",
-    "vue/arrow-spacing": "off",
-    "vue/block-spacing": "off",
-    "vue/block-tag-newline": "off",
-    "vue/brace-style": "off",
-    "vue/comma-dangle": "off",
-    "vue/comma-spacing": "off",
-    "vue/comma-style": "off",
-    "vue/dot-location": "off",
-    "vue/func-call-spacing": "off",
-    "vue/html-closing-bracket-newline": "off",
-    "vue/html-closing-bracket-spacing": "off",
-    "vue/html-end-tags": "off",
-    "vue/html-indent": "off",
-    "vue/html-quotes": "off",
-    "vue/key-spacing": "off",
-    "vue/keyword-spacing": "off",
-    "vue/max-attributes-per-line": "off",
-    "vue/multiline-html-element-content-newline": "off",
-    "vue/multiline-ternary": "off",
-    "vue/mustache-interpolation-spacing": "off",
-    "vue/no-extra-parens": "off",
-    "vue/no-multi-spaces": "off",
-    "vue/no-spaces-around-equal-signs-in-attribute": "off",
-    "vue/object-curly-newline": "off",
-    "vue/object-curly-spacing": "off",
-    "vue/object-property-newline": "off",
-    "vue/operator-linebreak": "off",
-    "vue/quote-props": "off",
-    "vue/script-indent": "off",
-    "vue/singleline-html-element-content-newline": "off",
-    "vue/space-in-parens": "off",
-    "vue/space-infix-ops": "off",
-    "vue/space-unary-ops": "off",
-    "vue/template-curly-spacing": "off",
-  },
-  settings: {
-    "vue-i18n": {
-      localeDir: "./aleksis/core/frontend/messages/*.{json}",
-      messageSyntaxVersion: "^8.0.0",
-    },
-  },
-  env: {
-    es2021: true,
-  },
-  parserOptions: {
-    ecmaVersion: "latest",
-  },
-};
diff --git a/.gitignore b/.gitignore
index fc620d9045075bdec195fc0bd15702daa2b3e408..613db10966de0e5b57a87f57c6fd586925ade0b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,6 +76,8 @@ docs/_build/
 .dev-js/.yarn
 .dev-js/.pnp.cjs
 .dev-js/.pnp.loader.mjs
+.dev-js/.yarnrc.yml
+.dev-js/schema.json
 
 # Lock files
 poetry.lock
diff --git a/aleksis/core/frontend/components/app/customMenu.graphql b/aleksis/core/frontend/components/app/customMenu.graphql
index 0d62c9dba32f931233ccf283dab89b3191a2ff8a..20fed201f1270c47a9afadb9fa96ddb73f9ca6fc 100644
--- a/aleksis/core/frontend/components/app/customMenu.graphql
+++ b/aleksis/core/frontend/components/app/customMenu.graphql
@@ -1,7 +1,9 @@
 query customMenu($name: String!) {
   customMenuByName(name: $name) {
+    id
     name
     items {
+      id
       name
       url
       icon
diff --git a/aleksis/core/frontend/components/app/whoAmI.graphql b/aleksis/core/frontend/components/app/whoAmI.graphql
index 159e4c088e1ba1327130711f9fac9e8d1645b607..1af27f4aeb41dcbe1e2722895de0019806762e45 100644
--- a/aleksis/core/frontend/components/app/whoAmI.graphql
+++ b/aleksis/core/frontend/components/app/whoAmI.graphql
@@ -6,6 +6,7 @@ query whoAmI($permissions: [String]!) {
     isAnonymous
     isImpersonate
     person {
+      id
       photo {
         url
       }
diff --git a/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql b/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql
index 9c1063d0686553cb07beba2beff1ad5b15044455..8a48c410a6dfec246b755688591fbee047aae170 100644
--- a/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql
+++ b/aleksis/core/frontend/components/authorized_oauth_applications/revokeOauthToken.graphql
@@ -1,5 +1,5 @@
 mutation revokeOauthTokens($ids: [ID]!) {
   revokeOauthTokens(ids: $ids) {
-    revokationCount
+    ok
   }
 }
diff --git a/aleksis/core/frontend/components/generic/forms/GroupField.vue b/aleksis/core/frontend/components/generic/forms/GroupField.vue
index 3433c5c3fb0695cfd5842ad0a461ab5cfb6aa765..736425d59cfc3e49cad459cd94ca50ff77cc4952 100644
--- a/aleksis/core/frontend/components/generic/forms/GroupField.vue
+++ b/aleksis/core/frontend/components/generic/forms/GroupField.vue
@@ -13,7 +13,7 @@
 
 <script>
 import queryMixin from "../../../mixins/queryMixin.js";
-import { groups } from "./group.graphql";
+import { formGroups } from "./group.graphql";
 
 export default {
   name: "GroupField",
@@ -26,7 +26,7 @@ export default {
     gqlQuery: {
       type: Object,
       required: false,
-      default: () => groups,
+      default: () => formGroups,
     },
   },
 };
diff --git a/aleksis/core/frontend/components/generic/forms/PersonField.vue b/aleksis/core/frontend/components/generic/forms/PersonField.vue
index ab852a0475d69c4205b76376cbdcdbc7457aec08..4df1df64d9a33c0d1203af62df7be6e841f3d54d 100644
--- a/aleksis/core/frontend/components/generic/forms/PersonField.vue
+++ b/aleksis/core/frontend/components/generic/forms/PersonField.vue
@@ -12,7 +12,7 @@
 
 <script>
 import queryMixin from "../../../mixins/queryMixin.js";
-import { persons } from "./person.graphql";
+import { formPersons } from "./person.graphql";
 
 export default {
   name: "PersonField",
@@ -25,7 +25,7 @@ export default {
     gqlQuery: {
       type: Object,
       required: false,
-      default: () => persons,
+      default: () => formPersons,
     },
   },
   methods: {
diff --git a/aleksis/core/frontend/components/generic/forms/group.graphql b/aleksis/core/frontend/components/generic/forms/group.graphql
index 5a4906b549225bab5e2ba98e2ad681fb03d3583e..fcd97f2b49c19ca765b5d5d91b77c36c838bcf89 100644
--- a/aleksis/core/frontend/components/generic/forms/group.graphql
+++ b/aleksis/core/frontend/components/generic/forms/group.graphql
@@ -1,4 +1,4 @@
-query groups {
+query formGroups {
   items: groups {
     id
     shortName
diff --git a/aleksis/core/frontend/components/generic/forms/person.graphql b/aleksis/core/frontend/components/generic/forms/person.graphql
index 6985dc9428b9c171706e170a35d2bd38e038a7ff..b7aaae4f2e3a8ccc4c54e8f0962ed518d75c5317 100644
--- a/aleksis/core/frontend/components/generic/forms/person.graphql
+++ b/aleksis/core/frontend/components/generic/forms/person.graphql
@@ -1,4 +1,4 @@
-query persons {
+query formPersons {
   items: persons {
     id
     shortName
diff --git a/aleksis/core/frontend/components/group/GroupAvatarClickbox.vue b/aleksis/core/frontend/components/group/GroupAvatarClickbox.vue
index 160a07501b5d12433fed1d1ac1720e16ca7b0d7a..d78e68ba09bc0b1555fb0d980f555b4ef45fb566 100644
--- a/aleksis/core/frontend/components/group/GroupAvatarClickbox.vue
+++ b/aleksis/core/frontend/components/group/GroupAvatarClickbox.vue
@@ -1,9 +1,9 @@
 <template>
   <avatar-clickbox>
     <template #activator>
-      <avatar-content :imageUrl="url" class="rounded-circle" />
+      <avatar-content :image-url="url" class="rounded-circle" />
     </template>
-    <avatar-content :imageUrl="url" contain />
+    <avatar-content :image-url="url" contain />
   </avatar-clickbox>
 </template>
 
diff --git a/aleksis/core/frontend/components/group/GroupList.vue b/aleksis/core/frontend/components/group/GroupList.vue
index 0b7b5ac115d63a0d04bacaa3f6567786898d2048..cbe3c49dd09e2a16513e9574c81ee03bbf95cfa6 100644
--- a/aleksis/core/frontend/components/group/GroupList.vue
+++ b/aleksis/core/frontend/components/group/GroupList.vue
@@ -1,7 +1,7 @@
 <script>
 import CRUDList from "../generic/CRUDList.vue";
 
-import { deleteGroups, groups } from "./groupList.graphql";
+import { deleteGroups, groups } from "./groups.graphql";
 import CreateButton from "../generic/buttons/CreateButton.vue";
 import TableLink from "../generic/TableLink.vue";
 import AvatarContent from "../person/AvatarContent.vue";
@@ -73,7 +73,7 @@ export default {
     <template #avatarUrl="{ item }">
       <table-link :to="{ name: 'core.group', params: { id: item.id } }">
         <v-avatar class="my-1 me-2">
-          <avatar-content :imageUrl="item.avatarUrl" contain />
+          <avatar-content :image-url="item.avatarUrl" contain />
         </v-avatar>
       </table-link>
     </template>
diff --git a/aleksis/core/frontend/components/group/GroupMembers.vue b/aleksis/core/frontend/components/group/GroupMembers.vue
index d0dc18a3de9925e40ab2dedaf6bef59c201fb212..b841283bb7bf312ff654b234edb575130f3bf838 100644
--- a/aleksis/core/frontend/components/group/GroupMembers.vue
+++ b/aleksis/core/frontend/components/group/GroupMembers.vue
@@ -76,7 +76,7 @@ export default {
     <!-- eslint-disable-next-line vue/valid-v-slot -->
     <template #item.id="{ item }">
       <v-tooltip bottom>
-        <template v-slot:activator="{ on, attrs }">
+        <template #activator="{ on, attrs }">
           <secondary-action-button
             v-bind="attrs"
             v-on="on"
diff --git a/aleksis/core/frontend/components/group/groupList.graphql b/aleksis/core/frontend/components/group/groupList.graphql
deleted file mode 100644
index 7df6604b94608301147eec994d6f0add1b5d9013..0000000000000000000000000000000000000000
--- a/aleksis/core/frontend/components/group/groupList.graphql
+++ /dev/null
@@ -1,24 +0,0 @@
-query groups($orderBy: [String], $filters: JSONString) {
-  items: groups(orderBy: $orderBy, filters: $filters) {
-    id
-    shortName
-    name
-    avatarUrl
-    schoolTerm {
-      id
-      name
-    }
-    groupType {
-      id
-      name
-    }
-    canEdit
-    canDelete
-  }
-}
-
-mutation deleteGroups($ids: [ID]!) {
-  deleteGroups(ids: $ids) {
-    deletionCount
-  }
-}
diff --git a/aleksis/core/frontend/components/group/groups.graphql b/aleksis/core/frontend/components/group/groups.graphql
index 701ecde468d517da81410cb35faba0022c91fa09..cc5e716216d05589f05f2ea65a1655cbf07abb3e 100644
--- a/aleksis/core/frontend/components/group/groups.graphql
+++ b/aleksis/core/frontend/components/group/groups.graphql
@@ -1,3 +1,22 @@
+query groups($orderBy: [String], $filters: JSONString) {
+  items: groups(orderBy: $orderBy, filters: $filters) {
+    id
+    shortName
+    name
+    avatarUrl
+    schoolTerm {
+      id
+      name
+    }
+    groupType {
+      id
+      name
+    }
+    canEdit
+    canDelete
+  }
+}
+
 query groupById($id: ID) {
   object: groupById(id: $id) {
     id
diff --git a/aleksis/core/frontend/components/notifications/myNotifications.graphql b/aleksis/core/frontend/components/notifications/myNotifications.graphql
index 9fcddc77f1e1f50dac823d308851977bf5cc08b1..d51ef53f22a4724d33d97bd223281ff3e3d1a705 100644
--- a/aleksis/core/frontend/components/notifications/myNotifications.graphql
+++ b/aleksis/core/frontend/components/notifications/myNotifications.graphql
@@ -2,6 +2,7 @@ query myNotifications {
   myNotifications: whoAmI {
     id
     person {
+      id
       notifications {
         id
         title
diff --git a/aleksis/core/frontend/components/pdf/pdf.graphql b/aleksis/core/frontend/components/pdf/pdf.graphql
index 0a5f7eb34e4a6e482615131870bc1fbd3210ef98..e469aaaf5ffd4c5005eb603ce387f9f82cae15d0 100644
--- a/aleksis/core/frontend/components/pdf/pdf.graphql
+++ b/aleksis/core/frontend/components/pdf/pdf.graphql
@@ -1,5 +1,6 @@
 query pdf($id: ID!) {
   pdf: pdfById(id: $id) {
+    id
     file {
       url
     }
diff --git a/aleksis/core/frontend/components/person/PersonActions.vue b/aleksis/core/frontend/components/person/PersonActions.vue
index c9244a80b22a8c8e10b74b79f27e6219408f28bb..5bc9c70558462c10ee76507a94eb2118e76cf66c 100644
--- a/aleksis/core/frontend/components/person/PersonActions.vue
+++ b/aleksis/core/frontend/components/person/PersonActions.vue
@@ -101,7 +101,8 @@
 </template>
 
 <script>
-import { personActions, deletePersons } from "./personActions.graphql";
+import { personActions } from "./personActions.graphql";
+import { deletePersons } from "./personList.graphql";
 import DeleteDialog from "../generic/dialogs/DeleteDialog.vue";
 
 export default {
diff --git a/aleksis/core/frontend/components/person/personActions.graphql b/aleksis/core/frontend/components/person/personActions.graphql
index 1936545144501d6ab9565496a91f7ff30cc9f8c9..be1b9209a9a6a6b3938423e8ccb20eed016b9398 100644
--- a/aleksis/core/frontend/components/person/personActions.graphql
+++ b/aleksis/core/frontend/components/person/personActions.graphql
@@ -10,9 +10,3 @@ query personActions($id: ID!) {
     canImpersonatePerson
   }
 }
-
-mutation deletePersons($ids: [ID]!) {
-  deletePersons(ids: $ids) {
-    deletionCount
-  }
-}
diff --git a/aleksis/core/frontend/components/room/RoomChip.vue b/aleksis/core/frontend/components/room/RoomChip.vue
index 1e14588742a276ca1a57476fbc2558e726ba482f..6aa70777d217a3a54b1a6292f2df6d30d817e8ff 100644
--- a/aleksis/core/frontend/components/room/RoomChip.vue
+++ b/aleksis/core/frontend/components/room/RoomChip.vue
@@ -12,7 +12,7 @@ export default {
 
 <template>
   <v-tooltip bottom>
-    <template v-slot:activator="{ on, attrs }">
+    <template #activator="{ on, attrs }">
       <v-chip v-bind="{ ...attrs, ...$attrs }" v-on="{ ...on, ...$listeners }">
         <v-avatar>
           <v-icon> mdi-door </v-icon>
diff --git a/aleksis/core/frontend/components/school_term/ActiveSchoolTermSelect.vue b/aleksis/core/frontend/components/school_term/ActiveSchoolTermSelect.vue
index 3de29edcf36b0661ee5a4dba6a59bee48c565183..e9c0543837faf74b89c6f314bc11c8079bdc70a6 100644
--- a/aleksis/core/frontend/components/school_term/ActiveSchoolTermSelect.vue
+++ b/aleksis/core/frontend/components/school_term/ActiveSchoolTermSelect.vue
@@ -1,7 +1,7 @@
 <script>
 import {
   activeSchoolTerm,
-  schoolTerms,
+  schoolTermsForActiveSchoolTerm,
   setActiveSchoolTerm,
 } from "./activeSchoolTerm.graphql";
 import loadingMixin from "../../mixins/loadingMixin";
@@ -10,7 +10,7 @@ export default {
   mixins: [loadingMixin],
   apollo: {
     schoolTerms: {
-      query: schoolTerms,
+      query: schoolTermsForActiveSchoolTerm,
     },
     activeSchoolTerm: {
       query: activeSchoolTerm,
diff --git a/aleksis/core/frontend/components/school_term/activeSchoolTerm.graphql b/aleksis/core/frontend/components/school_term/activeSchoolTerm.graphql
index 95b5fc170064e4dea9a24a17f3b10ad4bccb5af8..f005e1488979c348af957b4f09710cff9c254032 100644
--- a/aleksis/core/frontend/components/school_term/activeSchoolTerm.graphql
+++ b/aleksis/core/frontend/components/school_term/activeSchoolTerm.graphql
@@ -10,7 +10,7 @@ query activeSchoolTerm {
   }
 }
 
-query schoolTerms {
+query schoolTermsForActiveSchoolTerm {
   schoolTerms {
     id
     name
diff --git a/tox.ini b/tox.ini
index ffdea8542dd406b1b9a03f349f3828e0394a7fb3..a819cfbc366ae493cc345f3df57db89e9b19b4fa 100644
--- a/tox.ini
+++ b/tox.ini
@@ -24,13 +24,13 @@ setenv =
 
 [testenv:lint]
 commands_pre =
-    poetry install --only=dev
+    poetry install
     yarnpkg --cwd=.dev-js
 commands =
     poetry run ruff check {posargs} aleksis/
     yarnpkg --cwd=.dev-js run prettier --ignore-path={toxinidir}/.prettierignore {posargs} --check ..
-    yarnpkg --cwd=.dev-js run eslint ../aleksis/**/*/frontend/**/*.{js,vue} --config={toxinidir}/.eslintrc.js --resolve-plugins-relative-to=.
-
+    poetry run aleksis-admin graphql_schema --schema aleksis.core.schema.schema --out .dev-js/schema.json
+    yarnpkg --cwd=.dev-js run eslint ../aleksis/**/*/frontend/**/*.{js,vue,graphql} --config={toxinidir}/.dev-js/.eslintrc.js
 [testenv:security]
 commands_pre =
     poetry install --all-extras