diff --git a/web/services/model/vote-model.js b/web/services/model/vote-model.js
new file mode 100644
index 0000000000000000000000000000000000000000..ff95c026a82c0a003e43c934b2cbc37bcce019f8
--- /dev/null
+++ b/web/services/model/vote-model.js
@@ -0,0 +1,108 @@
+import * as Messages from "/services/messages/messages.js";
+
+let voteModel;
+
+export function getVoteModel() {
+  if (voteModel == null) {
+    voteModel = new VoteModel();
+  }
+  return voteModel;
+}
+
+class VoteModel {
+  constructor() {}
+
+  async getVote(id) {
+    if (this.votes == null) await this.refreshVotes();
+    let voteToGet;
+    this.votes.forEach((vote) => {
+      if (vote.ID == id) voteToGet = vote;
+    });
+    return voteToGet;
+  }
+
+  async getVotes() {
+    if (this.votes == null) {
+      try {
+        const response = await fetch("/api/Vote/", {
+          method: "GET",
+          headers: new Headers({
+            "XSRF-Token": this.current_user.xsrftoken,
+          }),
+        });
+        if (response.status !== 200) {
+          throw new Error(
+            `Votes could not be fetched (status ${response.status})`
+          );
+        }
+        this.votes = await response.json();
+      } catch (e) {
+        Messages.Show("is-warning", e.message);
+        console.error(e);
+      }
+    }
+    return this.votes;
+  }
+
+  async saveVote(
+    method,
+    ID,
+    DeskRoundID,
+    CandidateListID,
+    VoiceNumber,
+    Blank,
+    NullVote
+  ) {
+    try {
+      const response = await fetch("/api/Vote/" + ID, {
+        method: method,
+        headers: new Headers({
+          "XSRF-Token": this.current_user.xsrftoken,
+        }),
+        body: JSON.stringify({
+          ID: ID,
+          DeskRoundID: DeskRoundID,
+          CandidateListID: CandidateListID,
+          VoiceNumber: VoiceNumber,
+          Blank: Blank,
+          NullVote: NullVote,
+        }),
+      });
+      if (response.status !== 200) {
+        throw new Error(
+          `Vote could not be updated or created (status ${response.status})`
+        );
+      }
+      this.refreshVotes();
+      return await response.json();
+    } catch (e) {
+      Messages.Show("is-warning", e.message);
+      console.error(e);
+      return;
+    }
+  }
+
+  async deleteVote(ID) {
+    try {
+      const response = await fetch("/api/Vote/" + ID, {
+        method: "delete",
+        headers: new Headers({
+          "XSRF-Token": this.current_user.xsrftoken,
+        }),
+      });
+      if (response.status !== 200) {
+        throw new Error(
+          `Vote could not be deleted (status ${response.status})`
+        );
+      }
+    } catch (e) {
+      Messages.Show("is-warning", e.message);
+      console.error(e);
+    }
+  }
+
+  async refreshVotes() {
+    this.votes = null;
+    await this.getVotes();
+  }
+}