diff --git a/client/Signaler.js b/client/Signaler.js
new file mode 100644
index 0000000000000000000000000000000000000000..00fa58c9e97ccabdfd570669ddbdd89cdf4952ad
--- /dev/null
+++ b/client/Signaler.js
@@ -0,0 +1,245 @@
+export const baseURL = `ws://${document.location.hostname}:443/skribbl`;
+/**
+ * @typedef {{connection:RTCPeerConnection,dataChannel:RTCDataChannel}} ConnectionData 
+ */
+/**
+ * A connection to the signaling server used to start and join games.
+ */
+export default class Signaler {
+
+	/**
+	 * Starts a new game, returning a promise with its ID. After that, calls the given callback with a new RTCPeerConnection whenever someone joins the game.
+	 * @param {(connectionData:ConnectionData)=>void} onConnect
+	 */
+	static async host(onConnect){
+		const socket = new CustomSocket(baseURL+"/host");
+		//await socket.waitUntilReady();
+		let response = socket.next(message=>{
+			console.log("possible response:",message);
+			return message.startsWith("started ");
+		});
+		let id = (await response).split(" ")[1];
+		socket.onMessage(async message=>{
+			if (message.startsWith("join ")){
+				let playerID = message.split(" ",2)[1];
+				console.log("new player joined: ",playerID);
+				/** @type {((message:string)=>void)[]} */
+				let callbacks = [];
+				socket.onMessage(message=>{
+					if (message.startsWith("message "+playerID+" ")){
+						let data = message.split(" ").slice(2).join(" ");
+						console.log("message from player "+playerID+": ",data);
+						callbacks.forEach(callback=>callback(data));
+					}
+				});
+				onConnect(await perfectNegotiation(callback=>callbacks.push(callback),message=>{
+					console.log("message to player "+playerID+": ",message);
+					socket.send("message "+playerID+" "+message);
+				},false));
+			}
+		});
+		return id;
+	}
+
+	/**
+	 * Joins a new game, returning an RTCPeerConnection to its host.
+	 * @param {string} id
+	 */
+	static async join(id){
+		const socket = new CustomSocket(baseURL+"/join/"+id);
+		await socket.waitUntilReady();
+		/** @type {((message:string)=>void)[]} */
+		let callbacks = [];
+		socket.onMessage(message=>{
+			console.log("new message: ",message);
+			callbacks.forEach(callback=>callback(message.split(" ").slice(1).join(" ")));
+		});
+		return await perfectNegotiation(callback=>callbacks.push(callback),message=>{
+			console.log("send message:",message);
+			socket.send("message "+message);
+		},true);
+	}
+}
+/** 
+ * An implementation of "perfect negotiaton" as described [here](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Perfect_negotiation).
+ * @param {(callback:(message:string)=>void)=>void} onMessage
+ * @param {(message:string)=>void} sendMessage
+ * @param {boolean} polite
+ * @returns {Promise<ConnectionData>}
+ */
+export async function perfectNegotiation(onMessage,sendMessage,polite){
+	const connection = new RTCPeerConnection();
+	const dataChannel = connection.createDataChannel("channel",{id:0,negotiated:true});
+	let makingOffer = false;
+	connection.onnegotiationneeded = async()=>{
+		makingOffer = true;
+		// @ts-ignore
+		await connection.setLocalDescription();
+		//console.log("local description:",connection.localDescription)
+		sendMessage(JSON.stringify({description:connection.localDescription}));
+		makingOffer = false;
+	};
+	connection.onicecandidate = ({candidate})=>{
+		//console.log("candidate:",candidate);
+		sendMessage(JSON.stringify({candidate}));
+	};
+	let ignoreOffer = false;
+	onMessage(async message=>{
+		console.log("message to negotiator:",message);
+		/** @type {{description:RTCSessionDescriptionInit,candidate:undefined}|{candidate:RTCIceCandidateInit,description:undefined}} */
+		let data = JSON.parse(message);
+		if (data.description){
+			let description = data.description;
+			ignoreOffer = !polite&&(description.type=="offer"&&(makingOffer||connection.signalingState!="stable"));
+			if(!ignoreOffer){
+				await connection.setRemoteDescription(description);
+				if (description.type=="offer"){
+					// @ts-ignore
+					await connection.setLocalDescription();
+					sendMessage(JSON.stringify({description:connection.localDescription}));
+				}
+			}
+		}else if(data.candidate){
+			try {
+				await connection.addIceCandidate(data.candidate);
+			}catch(e){
+				if (!ignoreOffer){
+					throw e;
+				}
+			}
+		}
+	});
+	/** @type {Promise<ConnectionData>} */
+	return new Promise(resolve=>{
+		connection.onsignalingstatechange = ()=>{
+			console.log("Signaling state changed to:",connection.signalingState);
+			if (connection.signalingState=="stable"){
+				resolve({connection,dataChannel});
+			}
+		};
+	});
+}
+/**
+ * Custom socket class to pass strings to and from a server.
+ * 
+ * Originally, the idea was to let this pass json data instead, but since json is difficult to work with in java I've stuck with normal strings for now.
+ */
+export class CustomSocket {
+	/**
+	 * @callback MessageCallback
+	 * @param {string} message
+	 * @param {()=>void} remove removes this callback again
+	 * @returns {void}
+	 */
+	/**
+	 * @callback ErrorCallback
+	 * @param {Event} error
+	 * @param {()=>void} remove removes this callback again
+	 * @returns {void}
+	 */
+	/**
+	 * Creates a new custom socket.
+	 * @param {string} url
+	 */
+	constructor(url){
+		/** @type {MessageCallback[]} */
+		this._onMessageCallbacks = [];
+		/** @type {ErrorCallback[]} */
+		this._onErrorCallbacks = [];
+		this._webSocket = new WebSocket(url);
+		/** @type {Promise<void>} */
+		this._readyPromise = new Promise((resolve,reject)=>{
+			this._webSocket.onopen = ()=>resolve();
+			this._webSocket.addEventListener("error",reject,{once:true});
+		});
+		this._webSocket.onmessage = (e)=>{
+			console.log(`onmessage event! e.data: "${e.data}", registered callbacks: ${this._onMessageCallbacks.length}`);
+			this._onMessageCallbacks.slice().forEach(callback=>callback(e.data+"",()=>this.removeMessageCallback(callback)));
+		};
+		this._webSocket.onerror = (e)=>{
+			this._onErrorCallbacks.slice().forEach(callback=>callback(e,()=>this.removeErrorCallback(callback)));
+		};
+	}
+
+	async waitUntilReady(){
+		return this._readyPromise;
+	}
+
+	/**
+	 * Sends the given object as json to the server.
+	 * @param {string} data
+	 */
+	send(data){
+		this._webSocket.send(data);
+	}
+
+	/**
+	 * Registers a callback to be called whenever a message is received.
+	 * @param {MessageCallback} callback
+	 */
+	onMessage(callback){
+		this._onMessageCallbacks.push(callback);
+	}
+
+	/**
+	 * Removes a callback registered using `onMessage(callback)`.
+	 * @param {MessageCallback} callback
+	 */
+	removeMessageCallback(callback){
+		let index = this._onMessageCallbacks.indexOf(callback);
+		if (index!==-1){
+			this._onMessageCallbacks.splice(index,1);
+		}
+	}
+
+	/**
+	 * Returns a Promise that resolves to the next message received that passen the given filter, or `null` if no such message is received in the specified timeframe.
+	 * If an error occurs before, the promise will instead be rejected with that.
+	 * @param {(message:string)=>boolean} filter
+	 * @param {number} time time to wait before just returning `null`, in seconds. Defaults to 30. If the given number is smaller than or equal to 0, it gets treated as `Infinity`.
+	 * @return {Promise<string>}
+	 */
+	async next(filter=null,time=30){
+		return new Promise((resolve,reject)=>{
+			this.onMessage((message,remove)=>{
+				if (!filter||filter(message)){
+					resolve(message);
+					remove();
+				}
+			});
+			this.onError((error,remove)=>{
+				reject(error);
+				remove();
+			});
+			if (time>0){
+				setTimeout(()=>resolve(null),time*1000);
+			}
+		});
+	}
+
+	/**
+	 * Registers a callback to be called whenever there's an error.
+	 * @param {ErrorCallback} callback
+	 */
+	onError(callback){
+		this._onErrorCallbacks.push(callback);
+	}
+
+	/**
+	 * Removes a callback registered using `onError(callback)`.
+	 * @param {ErrorCallback} callback
+	 */
+	removeErrorCallback(callback){
+		let index = this._onErrorCallbacks.indexOf(callback);
+		if (index!==-1){
+			this._onErrorCallbacks.splice(index,1);
+		}
+	}
+
+	/**
+	 * Closes the connection.
+	 */
+	close(){
+		this._webSocket.close();
+	}
+}
\ No newline at end of file
diff --git a/client/index.html b/client/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..452c034b22eff17f258b86a7e54990cc9421d415
--- /dev/null
+++ b/client/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html>
+	<head>
+		<meta charset="UTF-8">
+		<title>signaling server test</title>
+		<script type="module" src="script.js"></script>
+		<style>
+			html, body {
+				width: 100%;
+				height: 100%;
+				margin: 0;
+				padding: 0;
+			}
+			#button {
+				padding: 10px 30px;
+				font-size: 30px;
+				background: orange;
+				color: white;
+				border: none;
+				border-radius: 4px;
+				position: absolute;
+				left: 50%;
+				top: 50%;
+				transform: translate(-50%,-50%);
+				cursor: pointer;
+			}
+			#button:disabled {
+				cursor: default;
+				background: #dfa580;
+			}
+		</style>
+	</head>
+	<body>
+		<button id="button">Host game</button>
+	</body>
+</html>
\ No newline at end of file
diff --git a/client/jsconfig.json b/client/jsconfig.json
new file mode 100644
index 0000000000000000000000000000000000000000..5e0ad8a2ba192134890b968f8734ab421a5a3fcb
--- /dev/null
+++ b/client/jsconfig.json
@@ -0,0 +1,7 @@
+{
+	"compilerOptions": {
+		"checkJs": true,
+		"target": "esnext",
+		"module": "esnext"
+	}
+}
\ No newline at end of file
diff --git a/client/script.js b/client/script.js
new file mode 100644
index 0000000000000000000000000000000000000000..7dfb1ddd05c64ee96f92bfe92ddee825fab68a1e
--- /dev/null
+++ b/client/script.js
@@ -0,0 +1,39 @@
+import Signaler from "./Signaler.js";
+
+document.addEventListener("DOMContentLoaded",async()=>{
+	if (document.location.hash){
+		document.body.innerHTML = "";
+		const gameID = document.location.hash.substring(1);
+		const {connection,dataChannel} = await Signaler.join(gameID);
+		console.group("Joined game "+gameID+"!");
+		console.log("connection:",connection);
+		console.log("dataChannel:",dataChannel);
+		console.groupEnd();
+		dataChannel.onmessage = (e)=>{
+			console.log("Message through DataChannel:",e.data);
+			dataChannel.send("Got ya message, many thanks <3");
+		}
+	}else{
+		/** @type {HTMLButtonElement} *///@ts-ignore
+		const button = document.getElementById("button");
+		button.addEventListener("click",async e=>{
+			button.disabled = true;
+			let id = await Signaler.host(async({connection,dataChannel})=>{
+				console.group("New connection!");
+				console.log("connection:",connection);
+				console.log("dataChannel:",dataChannel);
+				console.groupEnd();
+				dataChannel.onmessage = (e)=>{
+					console.log("Message through DataChannel:",e.data);
+				}
+				if (dataChannel.readyState!="open"){
+					await new Promise(resolve=>{
+						dataChannel.onopen = resolve;
+					});
+				}
+				dataChannel.send("Oh hello there :3");
+			});
+			alert(document.location.host+document.location.pathname+"#"+id);
+		});
+	}
+});
\ No newline at end of file
diff --git a/httpServer.bat b/httpServer.bat
new file mode 100644
index 0000000000000000000000000000000000000000..1c0e725fde04e78b995d12914eadb78c40ab88f1
--- /dev/null
+++ b/httpServer.bat
@@ -0,0 +1,3 @@
+cd client
+php -S localhost:8080
+pause
\ No newline at end of file
diff --git a/server/.classpath b/server/.classpath
new file mode 100644
index 0000000000000000000000000000000000000000..31a486a6584a929f0a3981b2d6eff92e740b4581
--- /dev/null
+++ b/server/.classpath
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+	<classpathentry kind="src" path="src"/>
+	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER">
+		<attributes>
+			<attribute name="module" value="true"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="lib" path="lib/Java-WebSocket-1.5.1-with-dependencies.jar">
+		<attributes>
+			<attribute name="javadoc_location" value="jar:file:/C:/Users/Ben%20Eltschig/Documents/Schule/Uni/Skribbl/server/lib/Java-WebSocket-1.5.1-javadoc.jar!/"/>
+		</attributes>
+	</classpathentry>
+	<classpathentry kind="output" path="bin"/>
+</classpath>
diff --git a/server/.gitignore b/server/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..ae3c1726048cd06b9a143e0376ed46dd9b9a8d53
--- /dev/null
+++ b/server/.gitignore
@@ -0,0 +1 @@
+/bin/
diff --git a/server/.project b/server/.project
new file mode 100644
index 0000000000000000000000000000000000000000..8c0e3ce4074d87cae544aa093dcfb10de7385bbd
--- /dev/null
+++ b/server/.project
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+	<name>Mathe-Skribbl</name>
+	<comment></comment>
+	<projects>
+	</projects>
+	<buildSpec>
+		<buildCommand>
+			<name>org.eclipse.jdt.core.javabuilder</name>
+			<arguments>
+			</arguments>
+		</buildCommand>
+	</buildSpec>
+	<natures>
+		<nature>org.eclipse.jdt.core.javanature</nature>
+	</natures>
+</projectDescription>
diff --git a/server/lib/Java-WebSocket-1.5.1-javadoc.jar b/server/lib/Java-WebSocket-1.5.1-javadoc.jar
new file mode 100644
index 0000000000000000000000000000000000000000..6048b4fe655dd9885b2c433d902f4fea6aaeba0f
Binary files /dev/null and b/server/lib/Java-WebSocket-1.5.1-javadoc.jar differ
diff --git a/server/lib/Java-WebSocket-1.5.1-with-dependencies.jar b/server/lib/Java-WebSocket-1.5.1-with-dependencies.jar
new file mode 100644
index 0000000000000000000000000000000000000000..8fb33ad5dca94eb91369b2cd939bdf4d2aa71500
Binary files /dev/null and b/server/lib/Java-WebSocket-1.5.1-with-dependencies.jar differ
diff --git a/server/src/Game.java b/server/src/Game.java
new file mode 100644
index 0000000000000000000000000000000000000000..97e4a7dc3e137f8e314e1f64bcecf1027e69f9ba
--- /dev/null
+++ b/server/src/Game.java
@@ -0,0 +1,68 @@
+import java.util.HashMap;
+import java.util.Map;
+
+import org.java_websocket.WebSocket;
+
+public class Game {
+	
+	public final String id;
+	public final WebSocket host;
+	private final Map<String,WebSocket> clients;
+	
+	public Game(String id, WebSocket host) {
+		this.id = id;
+		this.host = host;
+		this.clients = new HashMap<String,WebSocket>();
+	}
+	
+	void handleMessage(WebSocket socket, String message){
+		if (socket==this.host) {
+			String[] parts = message.split(" ",3);
+			WebSocket client = clients.get(parts[1]);
+			client.send("message "+parts[2]);
+			System.out.println("message from host "+this.id+" to player "+parts[1]+": "+parts[2]);
+		}else {
+			this.host.send("message "+getClientID(socket)+" "+message.split(" ",2)[1]);
+			System.out.println("message from player "+getClientID(socket)+" to host "+this.id+": "+message.split(" ",2)[1]);
+		}
+	}
+	
+	boolean hasClient(WebSocket client){
+		return clients.containsValue(client);
+	}
+	
+	void addClient(WebSocket client) {
+		String id = genID(6);
+		while (clients.containsKey(id)) {
+			id = genID(6);
+		}
+		clients.put(id,client);
+		this.host.send("join "+id);
+	}
+	
+	void removeClient(WebSocket client){
+		clients.values().remove(client);
+	}
+	
+	private String getClientID(WebSocket client) {
+		for (String id:clients.keySet()) {
+			if (clients.get(id)==client) {
+				return id;
+			}
+		}
+		return null;
+	}
+	
+	static String genID() {
+		return genID(8);
+	}
+	
+	static String genID(int n) {
+		String id = "";
+		for (int i=0;i<8;i++) {
+			char c = (char)(Math.random()*62);
+			id += (char)(c+(c<10?'0':(c<36?'a'-10:'A'-36)));
+		}
+		return id;
+	}
+}
diff --git a/server/src/SignalingServer.java b/server/src/SignalingServer.java
new file mode 100644
index 0000000000000000000000000000000000000000..09eb76d9e8060afe64600a764ec6872266ee965b
--- /dev/null
+++ b/server/src/SignalingServer.java
@@ -0,0 +1,108 @@
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.java_websocket.WebSocket;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.server.WebSocketServer;
+
+public class SignalingServer extends WebSocketServer {
+	
+	private Map<String,Game> games = new HashMap<String,Game>();
+	
+	public static void main(String[] args) {
+		InetSocketAddress address = new InetSocketAddress("localhost",443);
+		SignalingServer server = new SignalingServer(address);
+		server.run();
+	}
+
+	public SignalingServer(InetSocketAddress address) {
+		super(address);
+	}
+	
+	public void onStart() {
+		System.out.println("started server");
+	}
+	
+	public void onOpen(WebSocket socket, ClientHandshake handshake) {
+		String path = socket.getResourceDescriptor();
+		System.out.println("new connection to "+socket.getRemoteSocketAddress()+", path: "+path);
+		if (path.startsWith("/skribbl/host")) {
+			Game game = new Game(genUnusedID(),socket);
+			games.put(game.id,game);
+			socket.send("started "+game.id);
+			System.out.println("started game "+game.id);
+		}else if(path.startsWith("/skribbl/join/")) {
+			Game game = games.get(path.split("/",4)[3]);
+			if (game==null) {
+				socket.send("nope");
+				socket.close(4000,"invalid game code");
+			}else {
+				game.addClient(socket);
+			}
+		}else {
+			System.err.println("invalid connection attempt \""+path+"\"");
+			socket.send("nope");
+			socket.close(4000,"invalid request path");
+		}
+	}
+	
+	public void onClose(WebSocket socket, int code, String reason, boolean remote) {
+		System.out.println("closed " + socket.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
+		Game game = gameWithClient(socket);
+		if (game!=null) {
+			if (game.host==socket) {
+				games.remove(game.id);
+				System.out.println("closed game "+game.id);
+			}else {
+				game.removeClient(socket);
+			}
+		}
+	}
+	
+	public void onMessage(WebSocket socket, String message) {
+		System.out.println("received message from "+socket.getRemoteSocketAddress()+": \""+message+"\"");
+		if(message.startsWith("message ")) {
+			Game game = gameWithClient(socket);
+			if (game==null) {
+				socket.close(4000,"not currently in any game");
+			}else {
+				game.handleMessage(socket,message);
+			}
+		}else {
+			System.err.println("can't interpret message: \""+message+"\"");
+		}
+	}
+	
+	public void onMessage(WebSocket socket, ByteBuffer message) {
+		socket.close(1003);
+	}
+	
+	public void onError(WebSocket conn, Exception ex) {
+		if (conn==null) {
+			System.err.print("an error occured: ");
+		}else {
+			System.err.print("an error occurred on connection " + conn.getRemoteSocketAddress()  + ": ");
+		}
+		ex.printStackTrace();
+	}
+	
+	private Game gameWithClient(WebSocket socket) {
+		for (Game game:games.values()) {
+			if (game.host==socket||game.hasClient(socket)) {
+				return game;
+			}
+		}
+		return null;
+	}
+	
+	private String genUnusedID(){
+		String id = Game.genID();
+		while(games.containsKey(id)) {
+			id = Game.genID();
+		}
+		return id;
+	}
+	
+}