diff --git a/client/src/actions/presentation.ts b/client/src/actions/presentation.ts
index 0c3f2466556ab573341d190c14f83a198c41fcdb..0faa64a2bea151a86edb563eb4bf52902d49e902 100644
--- a/client/src/actions/presentation.ts
+++ b/client/src/actions/presentation.ts
@@ -3,8 +3,8 @@ This file handles actions for the presentation redux state
 */
 
 import axios from 'axios'
-import { Timer } from '../interfaces/Timer'
-import store, { AppDispatch, RootState } from './../store'
+import { TimerState } from '../interfaces/Timer'
+import { AppDispatch, RootState } from './../store'
 import Types from './types'
 
 /** Save competition in presentation state from input id */
@@ -35,18 +35,8 @@ export const setCurrentSlideByOrder = (order: number) => (dispatch: AppDispatch,
 export const setPresentationCode = (code: string) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_CODE, payload: code })
 }
+
 /** Set timer to input value */
-export const setPresentationTimer = (timer: Timer) => (dispatch: AppDispatch) => {
+export const setPresentationTimer = (timer: TimerState) => (dispatch: AppDispatch) => {
   dispatch({ type: Types.SET_PRESENTATION_TIMER, payload: timer })
 }
-
-/** Decrement timer */
-export const setPresentationTimerDecrement = () => (dispatch: AppDispatch) => {
-  dispatch({
-    type: Types.SET_PRESENTATION_TIMER,
-    payload: {
-      enabled: store.getState().presentation.timer.enabled,
-      value: store.getState().presentation.timer.value - 1,
-    },
-  })
-}
diff --git a/client/src/interfaces/Timer.ts b/client/src/interfaces/Timer.ts
index 49d1909e15692e68bb8cdef32ddd9f59d6b69409..03704c834d1b51075916d4bd4be3dcc99f9b0e53 100644
--- a/client/src/interfaces/Timer.ts
+++ b/client/src/interfaces/Timer.ts
@@ -1,4 +1,4 @@
-export interface Timer {
+export interface TimerState {
+  value: number | null
   enabled: boolean
-  value: number
 }
diff --git a/client/src/pages/admin/AdminPage.tsx b/client/src/pages/admin/AdminPage.tsx
index 1609384136c90a68e228f84ef81fe00193ab09ac..f77051b12decd0a0760d490d15ea12b1f135cdda 100644
--- a/client/src/pages/admin/AdminPage.tsx
+++ b/client/src/pages/admin/AdminPage.tsx
@@ -88,7 +88,6 @@ const AdminView: React.FC = () => {
     let activeIndex
     if (isAdmin) activeIndex = menuAdminItems.findIndex((menuItem) => location.pathname.endsWith(menuItem.route))
     else activeIndex = menuEditorItems.findIndex((menuItem) => location.pathname.endsWith(menuItem.route))
-    console.log(activeIndex, isAdmin, location.pathname, menuEditorItems)
     if (activeIndex !== -1) setOpenIndex(activeIndex)
   }
 
diff --git a/client/src/pages/views/AudienceViewPage.tsx b/client/src/pages/views/AudienceViewPage.tsx
index af1f18b3df30473ee466cd4b0acdf708123d5d89..e78db8b0cc890b6bc0abd55c076d5375451b4f48 100644
--- a/client/src/pages/views/AudienceViewPage.tsx
+++ b/client/src/pages/views/AudienceViewPage.tsx
@@ -2,7 +2,7 @@ import { Snackbar, Typography } from '@material-ui/core'
 import { Alert } from '@material-ui/lab'
 import React, { useEffect, useState } from 'react'
 import { useAppSelector } from '../../hooks'
-import { socketConnect, socketJoinPresentation } from '../../sockets'
+import { socketConnect } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { PresentationBackground, PresentationContainer } from './styled'
 
@@ -15,7 +15,6 @@ const AudienceViewPage: React.FC = () => {
   useEffect(() => {
     if (code && code !== '') {
       socketConnect('Audience')
-      socketJoinPresentation()
     }
   }, [])
   if (activeViewTypeId) {
diff --git a/client/src/pages/views/JudgeViewPage.tsx b/client/src/pages/views/JudgeViewPage.tsx
index c25b09dd29c6fe7d9930fab352d1006b0ccee7c0..675da803377ce23134485d0c2bf0bbc9c57c87a4 100644
--- a/client/src/pages/views/JudgeViewPage.tsx
+++ b/client/src/pages/views/JudgeViewPage.tsx
@@ -5,7 +5,7 @@ import React, { useEffect, useState } from 'react'
 import { getPresentationCompetition } from '../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../hooks'
 import { RichSlide } from '../../interfaces/ApiRichModels'
-import { socketConnect, socketJoinPresentation } from '../../sockets'
+import { socketConnect } from '../../sockets'
 import { renderSlideIcon } from '../../utils/renderSlideIcon'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { SlideListItem } from '../presentationEditor/styled'
@@ -64,7 +64,6 @@ const JudgeViewPage: React.FC = () => {
   useEffect(() => {
     if (code && code !== '') {
       socketConnect('Judge')
-      socketJoinPresentation()
     }
   }, [])
   useEffect(() => {
@@ -75,12 +74,14 @@ const JudgeViewPage: React.FC = () => {
       dispatch(getPresentationCompetition(competitionId.toString()))
     }
   }, [operatorActiveSlideId])
-  useEffect(() => {
-    // Every second tic of the timer, load new answers
-    if (timer.value % 2 === 0 && competitionId) {
-      dispatch(getPresentationCompetition(competitionId.toString()))
-    }
-  }, [timer.value])
+  // useEffect(() => {
+  //   // Every second tic of the timer, load new answers
+  //   // TODO: use a set interval that updates every second ( look in Timer.tsx in clien/src/pages/views/components )
+  //   // Then clear interval when timer - Date.now() is negative
+  //   if (timer !== null && timer - (Date.now() % 2) === 0 && competitionId) {
+  //     dispatch(getPresentationCompetition(competitionId.toString()))
+  //   }
+  // }, [timer])
   return (
     <div style={{ height: '100%' }}>
       <JudgeAppBar position="fixed">
diff --git a/client/src/pages/views/OperatorViewPage.tsx b/client/src/pages/views/OperatorViewPage.tsx
index a23809e467ef8ece808da4cdedf25a6489a3aa1a..6193b4cec6787acd41ac97ea5d3aaa8f00d8fea6 100644
--- a/client/src/pages/views/OperatorViewPage.tsx
+++ b/client/src/pages/views/OperatorViewPage.tsx
@@ -31,15 +31,7 @@ import React, { useEffect, useState } from 'react'
 import { useHistory } from 'react-router-dom'
 import { useAppSelector } from '../../hooks'
 import { RichTeam } from '../../interfaces/ApiRichModels'
-import {
-  socketConnect,
-  socketEndPresentation,
-  socketSetSlide,
-  socketSetSlideNext,
-  socketSetSlidePrev,
-  socketStartPresentation,
-  socketStartTimer,
-} from '../../sockets'
+import { socketConnect, socketEndPresentation, socketSync } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { Center } from '../presentationEditor/components/styled'
 import Timer from './components/Timer'
@@ -62,11 +54,6 @@ import {
  *
  *  ===========================================
  *  TODO:
- *  - Instead of copying code for others to join the competition, copy URL.
- *
- *
- *  - Fix scoreboard
- *
  *  - When two userers are connected to the same Localhost:5000 and updates/starts/end competition it
  *    creates a bug where the competition can't be started.
  * ===========================================
@@ -108,9 +95,9 @@ const OperatorViewPage: React.FC = () => {
   const classes = useStyles()
   const teams = useAppSelector((state) => state.presentation.competition.teams)
   const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null)
-  const competitionId = useAppSelector((state) => state.competitionLogin.data?.competition_id)
   const presentation = useAppSelector((state) => state.presentation)
   const activeId = useAppSelector((state) => state.presentation.competition.id)
+  const timer = useAppSelector((state) => state.presentation.timer)
   const history = useHistory()
   const viewTypes = useAppSelector((state) => state.types.viewTypes)
   const activeViewTypeId = viewTypes.find((viewType) => viewType.name === 'Audience')?.id
@@ -119,10 +106,13 @@ const OperatorViewPage: React.FC = () => {
     (state) =>
       state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.order
   )
+  const slideTimer = useAppSelector((state) =>
+    activeSlideOrder !== undefined ? state.presentation.competition.slides[activeSlideOrder].timer : null
+  )
+  const isFirstSlide = activeSlideOrder === 0
+  const isLastSlide = useAppSelector((state) => activeSlideOrder === state.presentation.competition.slides.length - 1)
   useEffect(() => {
     socketConnect('Operator')
-    socketSetSlide
-    setTimeout(startCompetition, 1000) // Wait for socket to connect
   }, [])
 
   /** Handles the browsers back button and if pressed cancels the ongoing competition */
@@ -141,12 +131,6 @@ const OperatorViewPage: React.FC = () => {
     setAnchorEl(null)
   }
 
-  const startCompetition = () => {
-    socketStartPresentation() // Calls the socket to start competition
-    console.log('started competition for')
-    console.log(competitionId)
-  }
-
   /** Making sure the user wants to exit the competition by displaying a dialog box */
   const handleVerifyExit = () => {
     setOpen(true)
@@ -208,6 +192,23 @@ const OperatorViewPage: React.FC = () => {
     return totalScore
   }
 
+  const handleStartTimer = () => {
+    if (!slideTimer) return
+
+    if (!timer.enabled) socketSync({ timer: { value: Date.now() + 1000 * slideTimer, enabled: true } })
+    else socketSync({ timer: { ...timer, enabled: false } })
+  }
+
+  const handleSetNextSlide = () => {
+    if (activeSlideOrder !== undefined)
+      socketSync({ slide_order: activeSlideOrder + 1, timer: { value: null, enabled: false } })
+  }
+
+  const handleSetPrevSlide = () => {
+    if (activeSlideOrder !== undefined)
+      socketSync({ slide_order: activeSlideOrder - 1, timer: { value: null, enabled: false } })
+  }
+
   return (
     <OperatorContainer>
       <Dialog open={openAlertCode} onClose={handleClose} aria-labelledby="max-width-dialog-title" maxWidth="xl">
@@ -301,17 +302,23 @@ const OperatorViewPage: React.FC = () => {
       <OperatorFooter>
         <ToolBarContainer>
           <Tooltip title="Föregående" arrow>
-            <OperatorButton onClick={socketSetSlidePrev} variant="contained">
+            <OperatorButton onClick={handleSetPrevSlide} variant="contained" disabled={isFirstSlide}>
               <ChevronLeftIcon fontSize="large" />
             </OperatorButton>
           </Tooltip>
 
-          <Tooltip title="Starta Timer" arrow>
-            <OperatorButton onClick={socketStartTimer} variant="contained">
-              <TimerIcon fontSize="large" />
-              <Timer disableText />
-            </OperatorButton>
-          </Tooltip>
+          {slideTimer && (
+            <Tooltip title="Starta Timer" arrow>
+              <OperatorButton
+                onClick={handleStartTimer}
+                variant="contained"
+                disabled={timer.value !== null && !timer.enabled}
+              >
+                <TimerIcon fontSize="large" />
+                <Timer disableText />
+              </OperatorButton>
+            </Tooltip>
+          )}
 
           <Tooltip title="Ställning" arrow>
             <OperatorButton onClick={handleOpenPopover} variant="contained">
@@ -326,7 +333,7 @@ const OperatorViewPage: React.FC = () => {
           </Tooltip>
 
           <Tooltip title="Nästa" arrow>
-            <OperatorButton onClick={socketSetSlideNext} variant="contained">
+            <OperatorButton onClick={handleSetNextSlide} variant="contained" disabled={isLastSlide}>
               <ChevronRightIcon fontSize="large" />
             </OperatorButton>
           </Tooltip>
diff --git a/client/src/pages/views/TeamViewPage.tsx b/client/src/pages/views/TeamViewPage.tsx
index 2ebc09269095480fe231fc3e05f5900eaf57e62a..eb9cb5e6196361bc127bb932412e1a98df357b29 100644
--- a/client/src/pages/views/TeamViewPage.tsx
+++ b/client/src/pages/views/TeamViewPage.tsx
@@ -2,7 +2,7 @@ import { Snackbar } from '@material-ui/core'
 import { Alert } from '@material-ui/lab'
 import React, { useEffect, useState } from 'react'
 import { useAppSelector } from '../../hooks'
-import { socketConnect, socketJoinPresentation } from '../../sockets'
+import { socketConnect } from '../../sockets'
 import SlideDisplay from '../presentationEditor/components/SlideDisplay'
 import { OperatorContainer, OperatorHeader, PresentationBackground, PresentationContainer } from './styled'
 
@@ -24,7 +24,6 @@ const TeamViewPage: React.FC = () => {
   useEffect(() => {
     if (code && code !== '') {
       socketConnect('Team')
-      socketJoinPresentation()
     }
   }, [])
   return (
diff --git a/client/src/pages/views/components/Timer.tsx b/client/src/pages/views/components/Timer.tsx
index f96806e27bdad78856a0b0ac4e09dd20b5923098..620fcd3471f4b6ef04d1930f5dac71e824c52ecf 100644
--- a/client/src/pages/views/components/Timer.tsx
+++ b/client/src/pages/views/components/Timer.tsx
@@ -1,22 +1,6 @@
-import React, { useEffect } from 'react'
-import { setPresentationTimer, setPresentationTimerDecrement } from '../../../actions/presentation'
+import React, { useEffect, useState } from 'react'
+import { setPresentationTimer } from '../../../actions/presentation'
 import { useAppDispatch, useAppSelector } from '../../../hooks'
-import store from '../../../store'
-
-/* const mapStateToProps = (state: any) => {
-  return {
-    timer: state.presentation.timer,
-    timer_start_value: state.presentation.slide.timer,
-  }
-}
-
-const mapDispatchToProps = (dispatch: any) => {
-  return {
-    // tickTimer: () => dispatch(tickTimer(1)),
-  }
-} */
-
-let timerIntervalId: NodeJS.Timeout
 
 type TimerProps = {
   disableText?: boolean
@@ -24,27 +8,62 @@ type TimerProps = {
 
 const Timer = ({ disableText }: TimerProps) => {
   const dispatch = useAppDispatch()
-  const slide = store
-    .getState()
-    .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
-  const timerStartValue = slide?.timer
   const timer = useAppSelector((state) => state.presentation.timer)
+  const [remainingTimer, setRemainingTimer] = useState<number>(0)
+  const [timerIntervalId, setTimerIntervalId] = useState<NodeJS.Timeout | null>(null)
+  const slideTimer = useAppSelector(
+    (state) =>
+      state.presentation.competition.slides.find((slide) => slide.id === state.presentation.activeSlideId)?.timer
+  )
+
   useEffect(() => {
-    if (!slide || !slide.timer) return
-    dispatch(setPresentationTimer({ enabled: false, value: slide.timer }))
-  }, [timerStartValue])
+    if (slideTimer) setRemainingTimer(slideTimer)
+  }, [slideTimer])
 
   useEffect(() => {
-    if (timer.enabled) {
-      timerIntervalId = setInterval(() => {
-        dispatch(setPresentationTimerDecrement())
-      }, 1000)
-    } else {
-      clearInterval(timerIntervalId)
+    console.log(timer)
+    if (!timer.enabled) {
+      console.log('interval id: ', timerIntervalId)
+      console.log('slide timer: ', slideTimer)
+
+      if (timerIntervalId !== null) clearInterval(timerIntervalId)
+
+      console.log('timer enabled false')
+      console.log('timer: ', timer)
+
+      if (timer.value !== null) {
+        console.log('timer value not null')
+
+        setRemainingTimer(0)
+      } else if (slideTimer) {
+        console.log('timer value null and slideTimer has value')
+
+        setRemainingTimer(slideTimer * 1000)
+      }
+
+      return
     }
-  }, [timer.enabled])
 
-  return <div>{`${!disableText ? 'Tid kvar:' : ''} ${timer.value}`}</div>
+    setTimerIntervalId(
+      setInterval(() => {
+        console.log('interval tick')
+        if (timer.value === null) return
+        if (timer.enabled === false && timerIntervalId !== null) clearInterval(timerIntervalId)
+
+        if (timer.value - Date.now() < 0) {
+          console.log('timer reached zero')
+          setRemainingTimer(0)
+          dispatch(setPresentationTimer({ ...timer, enabled: false }))
+          if (timerIntervalId !== null) clearInterval(timerIntervalId)
+          return
+        }
+
+        setRemainingTimer(timer.value - Date.now())
+      }, 500)
+    )
+  }, [timer.enabled, slideTimer])
+
+  return <div>{`${!disableText ? 'Tid kvar:' : ''} ${Math.round(remainingTimer / 1000)}`}</div>
 }
 
 export default Timer
diff --git a/client/src/reducers/presentationReducer.test.ts b/client/src/reducers/presentationReducer.test.ts
index e0368f7fc342bcc2b241cbf6eca3d368d34d649f..10362e687c6ea431dfb221a2c995324d384c92f3 100644
--- a/client/src/reducers/presentationReducer.test.ts
+++ b/client/src/reducers/presentationReducer.test.ts
@@ -14,8 +14,8 @@ const initialState = {
   activeSlideId: -1,
   code: '',
   timer: {
+    value: null,
     enabled: false,
-    value: 0,
   },
 }
 
diff --git a/client/src/reducers/presentationReducer.ts b/client/src/reducers/presentationReducer.ts
index cc183fadade89bd7eb504e8b501be8deb3aa3b04..69c7817fac1b920d5d98e34a0b66be4f52615618 100644
--- a/client/src/reducers/presentationReducer.ts
+++ b/client/src/reducers/presentationReducer.ts
@@ -1,6 +1,6 @@
 import { AnyAction } from 'redux'
 import Types from '../actions/types'
-import { Timer } from '../interfaces/Timer'
+import { TimerState } from '../interfaces/Timer'
 import { RichCompetition } from './../interfaces/ApiRichModels'
 
 /** Define a type for the presentation state */
@@ -8,7 +8,7 @@ interface PresentationState {
   competition: RichCompetition
   activeSlideId: number
   code: string
-  timer: Timer
+  timer: TimerState
 }
 
 /** Define the initial values for the presentation state */
@@ -25,8 +25,8 @@ const initialState: PresentationState = {
   activeSlideId: -1,
   code: '',
   timer: {
+    value: null,
     enabled: false,
-    value: 0,
   },
 }
 
@@ -41,7 +41,7 @@ export default function (state = initialState, action: AnyAction) {
     case Types.SET_PRESENTATION_CODE:
       return {
         ...state,
-        code: action.payload,
+        code: action.payload as string,
       }
     case Types.SET_PRESENTATION_SLIDE_ID:
       return {
@@ -49,13 +49,9 @@ export default function (state = initialState, action: AnyAction) {
         activeSlideId: action.payload as number,
       }
     case Types.SET_PRESENTATION_TIMER:
-      const timer = action.payload as Timer
-      if (action.payload.value == 0) {
-        timer.enabled = false
-      }
       return {
         ...state,
-        timer,
+        timer: action.payload as TimerState,
       }
     default:
       return state
diff --git a/client/src/sockets.ts b/client/src/sockets.ts
index dabaf4b9c26cef497c179aad7df293cac9647bc7..35c9abc6a0f00b84824bf5367d3dba005da3f144 100644
--- a/client/src/sockets.ts
+++ b/client/src/sockets.ts
@@ -1,114 +1,47 @@
-/**
- * This is a comment on the module level, i.e. the entire file.
- *
- * For this to appear in the documentation this is needed at the bottom.
- * @module
- */
-
 import io from 'socket.io-client'
 import { setCurrentSlideByOrder, setPresentationTimer } from './actions/presentation'
-import { Timer } from './interfaces/Timer'
+import { TimerState } from './interfaces/Timer'
 import store from './store'
 
-interface SetSlideInterface {
-  slide_order: number
-}
-
-interface TimerInterface {
-  value: number
-  enabled: boolean
-}
-
-interface SetTimerInterface {
-  timer: TimerInterface
+interface SyncInterface {
+  slide_order?: number
+  timer?: TimerState
 }
 
 let socket: SocketIOClient.Socket
 
-/**
- * You can also comment functions, like usual. This will automatically appear
- * in the documentation, no more needed.
- */
 export const socketConnect = (role: 'Judge' | 'Operator' | 'Team' | 'Audience') => {
-  if (!socket) {
-    const token = localStorage[role]
-    socket = io('localhost:5000', {
-      transportOptions: {
-        polling: {
-          extraHeaders: {
-            Authorization: token,
-          },
+  if (socket) return
+
+  const token = localStorage[`${role}Token`]
+  socket = io('localhost:5000', {
+    transportOptions: {
+      polling: {
+        extraHeaders: {
+          Authorization: token,
         },
       },
-    })
-
-    socket.on('set_slide', (data: SetSlideInterface) => {
-      setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
-    })
-
-    socket.on('set_timer', (data: SetTimerInterface) => {
-      setPresentationTimer(data.timer)(store.dispatch)
-    })
-
-    socket.on('end_presentation', () => {
-      socket.disconnect()
-    })
-  }
-}
+    },
+  })
 
-export const socketStartPresentation = () => {
-  socket.emit('start_presentation', { competition_id: store.getState().presentation.competition.id })
-}
+  socket.on('sync', (data: SyncInterface) => {
+    // The order of these is important, for some reason
+    if (data.timer !== undefined) setPresentationTimer(data.timer)(store.dispatch)
+    if (data.slide_order !== undefined) setCurrentSlideByOrder(data.slide_order)(store.dispatch, store.getState)
+  })
 
-export const socketJoinPresentation = () => {
-  socket.emit('join_presentation', { code: store.getState().presentation.code }) // TODO: Send code gotten from auth/login/<code> api call
+  socket.on('end_presentation', () => {
+    socket.disconnect()
+  })
 }
 
 export const socketEndPresentation = () => {
-  socket.emit('end_presentation', { competition_id: store.getState().presentation.competition.id })
-}
-
-export const socketSetSlideNext = () => {
-  const activeSlide = store
-    .getState()
-    .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
-  if (!activeSlide) return
-  socketSetSlide(activeSlide.order + 1) // TODO: Check that this slide exists
+  socket.emit('end_presentation')
 }
 
-export const socketSetSlidePrev = () => {
-  const activeSlide = store
-    .getState()
-    .presentation.competition.slides.find((slide) => slide.id === store.getState().presentation.activeSlideId)
-  if (!activeSlide) return
-  socketSetSlide(activeSlide.order - 1) // TODO: Check that this slide exists
-}
-
-/**
- * You can also comment a function like, adding more information to either
- * the paramters or the return value.
- *
- * @param slide_order This is a parameter to the function.
- * @returns This function returns nothing.
- */
-export const socketSetSlide = (slide_order: number) => {
-  if (slide_order < 0 || store.getState().presentation.competition.slides.length <= slide_order) {
-    return
-  }
-
-  socket.emit('set_slide', {
-    competition_id: store.getState().presentation.competition.id,
-    slide_order: slide_order,
+export const socketSync = ({ slide_order, timer }: SyncInterface) => {
+  socket.emit('sync', {
+    slide_order,
+    timer,
   })
 }
-
-export const socketSetTimer = (timer: Timer) => {
-  socket.emit('set_timer', {
-    competition_id: store.getState().presentation.competition.id,
-    timer: timer,
-  })
-}
-
-export const socketStartTimer = () => {
-  socketSetTimer({ enabled: true, value: store.getState().presentation.timer.value })
-}
diff --git a/server/app/apis/auth.py b/server/app/apis/auth.py
index 26301d646c98f06fa641223e2e206e6eb544ab1f..e29fd4ee12e9426b6278856cd262faa2df16c2d4 100644
--- a/server/app/apis/auth.py
+++ b/server/app/apis/auth.py
@@ -8,9 +8,9 @@ from datetime import datetime, timedelta
 import app.core.http_codes as codes
 import app.database.controller as dbc
 from app.apis import item_response, protect_route, text_response
-from app.core import sockets
 from app.core.codes import verify_code
 from app.core.dto import AuthDTO
+from app.core.sockets import is_active_competition
 from app.database.models import User, Whitelist
 from flask import current_app, has_app_context
 from flask_jwt_extended import create_access_token, get_jti, get_raw_jwt
@@ -164,7 +164,7 @@ class AuthLoginCode(Resource):
         item_code = dbc.get.code_by_code(code)
 
         if item_code.view_type_id != 4:
-            if item_code.competition_id not in sockets.presentations:
+            if not is_active_competition(item_code.competition_id):
                 api.abort(codes.UNAUTHORIZED, "Competition not active")
 
         # Create jwt that is only valid for 8 hours
diff --git a/server/app/core/sockets.py b/server/app/core/sockets.py
index a1675b2f0211aa52ed6327a67b6c54a9ad2d70f4..be9873c7280684aa86b247ba3b7a2d376d472902 100644
--- a/server/app/core/sockets.py
+++ b/server/app/core/sockets.py
@@ -1,14 +1,10 @@
 """
-Contains all functionality related sockets. That is starting and ending a presentation, 
-joining and leaving a presentation and syncing slides and timer bewteen all clients
-connected to the same presentation.
+Contains all functionality related sockets. That is starting, joining, ending, 
+disconnecting from and syncing active competitions.
 """
 import logging
-from functools import wraps
-from typing import Dict
 
-from app.core import db
-from app.database.models import Code, Slide, ViewType
+from decorator import decorator
 from flask.globals import request
 from flask_jwt_extended import verify_jwt_in_request
 from flask_jwt_extended.utils import get_jwt_claims
@@ -18,258 +14,148 @@ logger = logging.getLogger(__name__)
 logger.propagate = False
 logger.setLevel(logging.INFO)
 
-formatter = logging.Formatter("[%(levelname)s] %(funcName)s: %(message)s")
+formatter = logging.Formatter("[%(levelname)s] %(message)s")
 stream_handler = logging.StreamHandler()
 stream_handler.setFormatter(formatter)
 logger.addHandler(stream_handler)
 
 sio = SocketIO(cors_allowed_origins="http://localhost:3000")
 
-presentations = {}
+active_competitions = {}
 
 
-def _is_allowed(allowed, actual):
-    return actual and "*" in allowed or actual in allowed
-
-
-def protect_route(allowed_views=None):
-    def wrapper(f):
-        @wraps(f)
-        def inner(*args, **kwargs):
-            try:
-                verify_jwt_in_request()
-            except:
-                logger.warning("Missing Authorization Header")
-                return
-
-            nonlocal allowed_views
-            allowed_views = allowed_views or []
-            claims = get_jwt_claims()
-            view = claims.get("view")
-            if not _is_allowed(allowed_views, view):
-                logger.warning(f"View '{view}' is not allowed to access route only accessible by '{allowed_views}'")
-                return
-
-            return f(*args, **kwargs)
-
-        return inner
-
-    return wrapper
-
-
-@sio.on("connect")
-def connect() -> None:
-    logger.info(f"Client '{request.sid}' connected")
-
-
-@sio.on("disconnect")
-def disconnect() -> None:
+def _unpack_claims():
     """
-    Remove client from the presentation it was in. Delete presentation if no
-    clients are connected to it.
+    :return: A tuple containing competition_id and view, gotten from claim
+    :rtype: tuple
     """
-    for competition_id, presentation in presentations.items():
-        if request.sid in presentation["clients"]:
-            del presentation["clients"][request.sid]
-            logger.debug(f"Client '{request.sid}' left presentation '{competition_id}'")
-            break
 
-    if presentations and not presentations[competition_id]["clients"]:
-        del presentations[competition_id]
-        logger.info(f"No people left in presentation '{competition_id}', ended presentation")
+    claims = get_jwt_claims()
+    return claims["competition_id"], claims["view"]
 
-    logger.info(f"Client '{request.sid}' disconnected")
 
-
-@protect_route(allowed_views=["Operator"])
-@sio.on("start_presentation")
-def start_presentation(data: Dict) -> None:
+def is_active_competition(competition_id):
     """
-    Starts a presentation if that competition is currently not active.
+    :return: True if competition with competition_id is currently active else False
+    :rtype: bool
     """
+    return competition_id in active_competitions
 
-    competition_id = data["competition_id"]
-
-    if competition_id in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to start competition '{competition_id}', presentation already active"
-        )
-        return
-
-    presentations[competition_id] = {
-        "clients": {request.sid: {"view_type": "Operator"}},
-        "slide": 0,
-        "timer": {"enabled": False, "start_value": None, "value": None},
-    }
-
-    join_room(competition_id)
-    logger.debug(f"Client '{request.sid}' joined room {competition_id}")
 
-    logger.info(f"Client '{request.sid}' started competition '{competition_id}'")
+def _get_sync_variables(active_competition, sync_values):
+    return {key: value for key, value in active_competition.items() if key in sync_values}
 
 
-@protect_route(allowed_views=["Operator"])
-@sio.on("end_presentation")
-def end_presentation(data: Dict) -> None:
+@decorator
+def authorize_client(f, allowed_views=None, require_active_competition=True, *args, **kwargs):
     """
-    End a presentation by sending end_presentation to all connected clients.
-
-    The only clients allowed to do this is the one that started the presentation.
-
-    Log error message if no presentation exists with the send id or if this
-    client is not in that presentation.
+    Decorator used to authorize a client that sends socket events. Check that
+    the client has authorization headers, that client view gotten from claims
+    is in allowed_views and that the competition the clients is in is active
+    if require_active_competition is True.
     """
-    competition_id = data["competition_id"]
-
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to end presentation '{competition_id}', no such presentation exists"
-        )
-        return
-
-    if request.sid not in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to end presentation '{competition_id}', client not in presentation"
-        )
-        return
 
-    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
-        logger.error(f"Client '{request.sid}' failed to end presentation '{competition_id}', client is not operator")
+    try:
+        verify_jwt_in_request()
+    except:
+        logger.error(f"Won't call function '{f.__name__}': Missing Authorization Header")
         return
 
-    del presentations[competition_id]
-    logger.debug(f"Deleted presentation {competition_id}")
-
-    emit("end_presentation", room=competition_id, include_self=True)
-    logger.debug(f"Emitting event 'end_presentation' to room {competition_id} including self")
-
-    logger.info(f"Client '{request.sid}' ended presentation '{competition_id}'")
-
-
-@sio.on("join_presentation")
-def join_presentation(data: Dict) -> None:
-    """
-    Join a currently active presentation.
-
-    Log error message if given code doesn't exist, if not presentation associated
-    with that code exists or if client is already in the presentation.
-    """
-    code = data["code"]
-    item_code = db.session.query(Code).filter(Code.code == code).first()
-
-    if not item_code:
-        logger.error(f"Client '{request.sid}' failed to join presentation with code '{code}', no such code exists")
-        return
+    def _is_allowed(allowed, actual):
+        return actual and "*" in allowed or actual in allowed
 
-    competition_id = item_code.competition_id
+    competition_id, view = _unpack_claims()
 
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to join presentation '{competition_id}', no such presentation exists"
-        )
+    if require_active_competition and not is_active_competition(competition_id):
+        logger.error(f"Won't call function '{f.__name__}': Competition '{competition_id}' is not active")
         return
 
-    if request.sid in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to join presentation '{competition_id}', client already in presentation"
-        )
+    allowed_views = allowed_views or []
+    if not _is_allowed(allowed_views, view):
+        logger.error(f"Won't call function '{f.__name__}': View '{view}' is not {' or '.join(allowed_views)}")
         return
 
-    # TODO: Write function in database controller to do this
-    view_type_name = db.session.query(ViewType).filter(ViewType.id == item_code.view_type_id).one().name
-
-    presentations[competition_id]["clients"][request.sid] = {"view_type": view_type_name}
+    return f(*args, **kwargs)
 
-    join_room(competition_id)
-    logger.debug(f"Client '{request.sid}' joined room {competition_id}")
 
-    logger.info(f"Client '{request.sid}' joined competition '{competition_id}'")
-
-    emit("set_slide", {"slide_order": presentations[competition_id]["slide"]})
-
-
-@protect_route(allowed_views=["Operator"])
-@sio.on("set_slide")
-def set_slide(data: Dict) -> None:
+@sio.event
+@authorize_client(require_active_competition=False, allowed_views=["*"])
+def connect() -> None:
     """
-    Sync slides between all clients in the same presentation by sending
-    set_slide to them.
-
-    Log error if the given competition_id is not active, if client is not in
-    that presentation or the client is not the one who started the presentation.
+    Connect to a active competition. If competition with competition_id is not active,
+    start it if client is an operator, otherwise ignore it.
     """
 
-    competition_id = data["competition_id"]
-    slide_order = data["slide_order"]
-
-    if competition_id not in presentations:
+    competition_id, view = _unpack_claims()
+
+    if is_active_competition(competition_id):
+        active_competition = active_competitions[competition_id]
+        active_competition["client_count"] += 1
+        join_room(competition_id)
+        emit("sync", _get_sync_variables(active_competition, ["slide_order", "timer"]))
+        logger.info(f"Client '{request.sid}' with view '{view}' joined competition '{competition_id}'")
+    elif view == "Operator":
+        join_room(competition_id)
+        active_competitions[competition_id] = {
+            "client_count": 1,
+            "slide_order": 0,
+            "timer": {
+                "value": None,
+                "enabled": False,
+            },
+        }
+        logger.info(f"Client '{request.sid}' with view '{view}' started competition '{competition_id}'")
+    else:
         logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', no such presentation exists"
+            f"Client '{request.sid}' with view '{view}' tried to join non active competition '{competition_id}'"
         )
-        return
 
-    if request.sid not in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client not in presentation"
-        )
-        return
 
-    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client is not operator"
-        )
-        return
+@sio.event
+@authorize_client(allowed_views=["*"])
+def disconnect() -> None:
+    """
+    Remove client from the active_competition it was in. Delete active_competition if no
+    clients are connected to it.
+    """
 
-    num_slides = db.session.query(Slide).filter(Slide.competition_id == competition_id).count()
+    competition_id, _ = _unpack_claims()
+    active_competitions[competition_id]["client_count"] -= 1
+    logger.info(f"Client '{request.sid}' disconnected from competition '{competition_id}'")
 
-    if not (0 <= slide_order < num_slides):
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', slide number {slide_order} does not exist"
-        )
-        return
+    if active_competitions[competition_id]["client_count"] <= 0:
+        del active_competitions[competition_id]
+        logger.info(f"No people left in active_competition '{competition_id}', ended active_competition")
 
-    presentations[competition_id]["slide"] = slide_order
 
-    emit("set_slide", {"slide_order": slide_order}, room=competition_id, include_self=True)
-    logger.debug(f"Emitting event 'set_slide' to room {competition_id} including self")
+@sio.event
+@authorize_client(allowed_views=["Operator"])
+def end_presentation() -> None:
+    """
+    End a active_competition by sending end_presentation to all connected clients.
+    """
 
-    logger.info(f"Client '{request.sid}' set slide '{slide_order}' in competition '{competition_id}'")
+    competition_id, _ = _unpack_claims()
+    emit("end_presentation", room=competition_id, include_self=True)
 
 
-@protect_route(allowed_views=["Operator"])
-@sio.on("set_timer")
-def set_timer(data: Dict) -> None:
+@sio.event
+@authorize_client(allowed_views=["Operator"])
+def sync(data) -> None:
     """
-    Sync slides between all clients in the same presentation by sending
-    set_timer to them.
-
-    Log error if the given competition_id is not active, if client is not in
-    that presentation or the client is not the one who started the presentation.
+    Sync active_competition for all clients connected to competition.
     """
-    competition_id = data["competition_id"]
-    timer = data["timer"]
-
-    if competition_id not in presentations:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', no such presentation exists"
-        )
-        return
-
-    if request.sid not in presentations[competition_id]["clients"]:
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client not in presentation"
-        )
-        return
 
-    if presentations[competition_id]["clients"][request.sid]["view_type"] != "Operator":
-        logger.error(
-            f"Client '{request.sid}' failed to set slide in presentation '{competition_id}', client is not operator"
-        )
-        return
+    competition_id, view = _unpack_claims()
+    active_competition = active_competitions[competition_id]
 
-    # TODO: Save timer in presentation, maybe?
+    for key, value in data.items():
+        if key not in active_competition:
+            logger.warning(f"Invalid sync data: '{key}':'{value}'")
 
-    emit("set_timer", {"timer": timer}, room=competition_id, include_self=True)
-    logger.debug(f"Emitting event 'set_timer' to room {competition_id} including self")
+        active_competition[key] = value
 
-    logger.info(f"Client '{request.sid}' set timer '{timer}' in presentation '{competition_id}'")
+    emit("sync", _get_sync_variables(active_competition, data), room=competition_id, include_self=True)
+    logger.info(
+        f"Client '{request.sid}' with view '{view}' synced values {_get_sync_variables(active_competition, data)} in competition '{competition_id}'"
+    )
diff --git a/server/requirements.txt b/server/requirements.txt
index 6a9e48fc29b527a9a3609716953d4ba5a52fe4e2..472b12cdf8229664d6d4bc679e6a85cb941341d7 100644
Binary files a/server/requirements.txt and b/server/requirements.txt differ
diff --git a/server/tests/test_app.py b/server/tests/test_app.py
index 880a41bd82a3b6cac8795a3cf61eaed6569dbadb..4b704bd7de51ebb624b8979d45ed4d17fe92032d 100644
--- a/server/tests/test_app.py
+++ b/server/tests/test_app.py
@@ -447,7 +447,7 @@ def test_authorization(client):
     add_default_values()
 
     # Fake that competition 1 is active
-    sockets.presentations[1] = {}
+    sockets.active_competitions[1] = {}
 
     #### TEAM ####
     # Login in with team code