diff --git a/wahlfang/settings/development.py b/wahlfang/settings/development.py
index 5471b5cbba39265c5b63a98c79a3253301def698..19ebd3e1ccf477b6fe828cb50096f6b6af49cc8e 100644
--- a/wahlfang/settings/development.py
+++ b/wahlfang/settings/development.py
@@ -32,6 +32,12 @@ EMAIL_SENDER = 'no-reply@stusta.de'
 EMAIL_PORT = 25
 
 # LDAP
+AUTHENTICATION_BACKENDS = {
+    'vote.authentication.AccessCodeBackend',
+    'management.authentication.ManagementBackend',
+    'management.authentication.ManagementBackendLDAP',  # this authentication backend must be enabled for ldap auth
+    'django.contrib.auth.backends.ModelBackend'
+}
 AUTH_LDAP_SERVER_URI = "ldap://ldap.stusta.de"
 AUTH_LDAP_USER_DN_TEMPLATE = "cn=%(user)s,ou=account,ou=pnyx,dc=stusta,dc=mhn,dc=de"
 AUTH_LDAP_START_TLS = True
diff --git a/wahlfang_api/serializers.py b/wahlfang_api/serializers.py
index 8a0f0205edcfb01afe4a8d300d1f61b3fe50eb88..c3cafc9dc22d1f550172ed98e5c33eaf55ee7a2b 100644
--- a/wahlfang_api/serializers.py
+++ b/wahlfang_api/serializers.py
@@ -93,6 +93,14 @@ class ElectionSerializer(serializers.ModelSerializer):
         return self.context['request'].user.can_vote(obj)
 
 
+class SpectatorElectionSerializer(serializers.ModelSerializer):
+    election_summary = ElectionSummarySerializer(source='public_election_summary', many=True, read_only=True)
+
+    class Meta:
+        model = Election
+        fields = '__all__'
+
+
 class SessionSerializer(serializers.ModelSerializer):
     class Meta:
         model = Session
@@ -105,3 +113,11 @@ class VoterDetailSerializer(serializers.ModelSerializer):
     class Meta:
         model = Voter
         exclude = ['logged_in', 'password']
+
+
+class SpectatorSessionSerializer(serializers.ModelSerializer):
+    elections = SpectatorElectionSerializer(many=True, read_only=True)
+
+    class Meta:
+        model = Session
+        fields = ['title', 'meeting_link', 'elections']
diff --git a/wahlfang_api/urls.py b/wahlfang_api/urls.py
index 4e250be9cdca227eb7b06e5aebf718f2c6d60f30..08aa32ddcb53682b27c3942f88d229f7536b7f09 100644
--- a/wahlfang_api/urls.py
+++ b/wahlfang_api/urls.py
@@ -4,11 +4,13 @@ from rest_framework_simplejwt.views import (
     TokenRefreshView,
     TokenVerifyView,
 )
+
 from wahlfang_api.views import (
     TokenObtainVoterView,
     TokenObtainElectionManagerView,
     ElectionViewset,
     VoterInfoView,
+    SpectatorView,
 )
 
 app_name = 'rest_api'
@@ -18,8 +20,8 @@ router.register('vote/elections', ElectionViewset)
 
 urlpatterns = [
     path('', include(router.urls)),
+    path('vote/spectator/<str:uuid>/', SpectatorView.as_view(), name='spectator'),
     path('vote/voter_info/', VoterInfoView.as_view(), name='voter_info'),
-    # path('vote/elections/', ElectionList.as_view(), name='election_list'),
     path('auth/code/token/', TokenObtainVoterView.as_view(), name='token_obtain_access_code'),
     path('auth/token/', TokenObtainElectionManagerView.as_view(), name='token_obtain_pair'),
     path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
diff --git a/wahlfang_api/views.py b/wahlfang_api/views.py
index e49d8b398d66e7601aea1f63cab70dec3d211f80..bde55eec1f329a77814afa92873578f84912c558 100644
--- a/wahlfang_api/views.py
+++ b/wahlfang_api/views.py
@@ -6,15 +6,17 @@ from rest_framework.response import Response
 from rest_framework.throttling import AnonRateThrottle
 from rest_framework_simplejwt.views import TokenViewBase
 
+from vote.forms import VoteForm
+from vote.models import Election, Voter, Application, Session
 from wahlfang_api.authentication import IsVoter
 from wahlfang_api.serializers import (
     TokenObtainVoterSerializer,
     TokenObtainElectionManagerSerializer,
     ElectionSerializer,
-    VoterDetailSerializer, EditApplicationSerializer
+    VoterDetailSerializer,
+    EditApplicationSerializer,
+    SpectatorSessionSerializer
 )
-from vote.forms import VoteForm
-from vote.models import Election, Voter, Application
 
 
 class TokenObtainVoterView(TokenViewBase):
@@ -44,6 +46,14 @@ class VoterInfoView(generics.RetrieveAPIView):
         return self.request.user
 
 
+class SpectatorView(generics.RetrieveAPIView):
+    queryset = Session.objects.all()
+    serializer_class = SpectatorSessionSerializer
+
+    def get_object(self):
+        return self.queryset.get(spectator_token=self.kwargs['uuid'])
+
+
 class ElectionViewset(viewsets.ReadOnlyModelViewSet):
     queryset = Election.objects.all()
     permission_classes = [IsVoter]
diff --git a/wahlfang_web/src/api/index.js b/wahlfang_web/src/api/index.js
index 94b19617248497b4966d74f57d010ae7e9639a13..926ac6c9f6a4408a5911c746ef0d50389ef44d14 100644
--- a/wahlfang_web/src/api/index.js
+++ b/wahlfang_web/src/api/index.js
@@ -85,6 +85,15 @@ export const logoutVoter = async () => {
     return true;
 }
 
+export const fetchSpectatorInfo = async (uuid) => {
+    const response = await makeRequest(`/vote/spectator/${uuid}/`, 'GET');
+    if (response.status === 200) {
+        return response.json();
+    } else {
+        throw Error("could not fetch spectator view")
+    }
+}
+
 export const fetchVoterInfo = async () => {
     const response = await makeAuthenticatedVoterRequest(voteAPIRoutes.voterInfo, 'GET');
     return await response.json();
diff --git a/wahlfang_web/src/components/SpectatorElection.js b/wahlfang_web/src/components/SpectatorElection.js
new file mode 100644
index 0000000000000000000000000000000000000000..716cdaa42abe2ede8d6f0f89a43cf267f346d23b
--- /dev/null
+++ b/wahlfang_web/src/components/SpectatorElection.js
@@ -0,0 +1,67 @@
+import React from "react";
+import moment from "moment";
+
+export default function SpectatorElection({election}) {
+    const isOpen = election.start_date !== null && (new Date(election.start_date) <= new Date() && (election.end_date === null || new Date(election.end_date) > new Date()));
+    const isClosed = election.end_date !== null && new Date(election.end_date) <= new Date();
+
+    return (
+        <div className="card mb-2">
+            <div className="card-body">
+                <h4 className="d-flex justify-content-between">
+                    <span>{election.title}</span>
+                </h4>
+                {election.end_date ? (
+                    <>
+                        <small className="text-muted">Voting Period: {moment(election.start_date).format('llll')}
+                            - {moment(election.end_date).format('llll')}</small>
+                    </>
+                ) : null}
+                <hr/>
+                <div className="list-group mt-3">
+                    {isClosed && election.result_published === '1' ? (
+                        <div className="alert alert-info" role="alert">
+                            <h4 className="alert-heading">Voting Ended:</h4>
+                            <hr/>
+                            <table className="table table-striped">
+                                <thead className="thead-dark">
+                                <tr>
+                                    <th scope="col">#</th>
+                                    <th scope="col">{election.voters_self_apply ? "Applicant" : "Option"}</th>
+                                    <th scope="col">Yes</th>
+                                    <th scope="col">No</th>
+                                    <th scope="col">Abstention</th>
+                                </tr>
+                                </thead>
+                                <tbody>
+                                {election.election_summary.map((application, index) => (
+                                    <tr key={index}>
+                                        <th scope="row">{index + 1}</th>
+                                        <td>{application.display_name}</td>
+                                        <td>{application.votes_accept}</td>
+                                        <td>{application.votes_reject}</td>
+                                        <td>{application.votes_abstention}</td>
+                                    </tr>
+                                ))}
+                                </tbody>
+                            </table>
+                        </div>
+                    ) : !isOpen && !isClosed ? (
+                        <button className="btn btn-outline-dark disabled">
+                            Election has not started yet.
+                        </button>
+                    ) : isOpen && !isClosed ? (
+                        <button className="btn btn-outline-dark disabled">
+                            Election is currently ongoing.
+                        </button>
+                    ) : (
+                        <button className="btn btn-outline-dark disabled">
+                            Election is over but results haven't been published yet. Please ask your election manager to
+                            do so and refresh the page.
+                        </button>
+                    )}
+                </div>
+            </div>
+        </div>
+    )
+}
\ No newline at end of file
diff --git a/wahlfang_web/src/pages/ManagementApp.js b/wahlfang_web/src/pages/ManagementApp.js
index 9e46f9a1a78702c9d1fc3f003587a24f658c97e0..6fc35e63f26743be26abc4f8845c0aa1115b252f 100644
--- a/wahlfang_web/src/pages/ManagementApp.js
+++ b/wahlfang_web/src/pages/ManagementApp.js
@@ -16,29 +16,27 @@ export default function ManagementApp() {
     const {path} = useRouteMatch();
 
     useEffect(() => {
-        if (loading && !authenticated) {
-            const authToken = loadManagerToken();
-            if (authToken && isTokenValid(authToken.access)) {
-                setAuthenticated(true);
-                setLoading(false);
-                managementWS.initWs();
-                console.log("found valid access token");
-            } else if (authToken && isTokenValid(authToken.refresh)) {
-                console.log("found valid refresh token");
-                refreshManagerToken()
-                    .then(() => {
-                        setAuthenticated(true);
-                        setLoading(false);
-                        managementWS.initWs();
-                    })
-                    .catch(() => {
-                        setLoading(false);
-                    })
-            } else {
-                setLoading(false);
-            }
+        const authToken = loadManagerToken();
+        if (authToken && isTokenValid(authToken.access)) {
+            setAuthenticated(true);
+            setLoading(false);
+            managementWS.initWs();
+            console.log("found valid access token");
+        } else if (authToken && isTokenValid(authToken.refresh)) {
+            console.log("found valid refresh token");
+            refreshManagerToken()
+                .then(() => {
+                    setAuthenticated(true);
+                    setLoading(false);
+                    managementWS.initWs();
+                })
+                .catch(() => {
+                    setLoading(false);
+                })
+        } else {
+            setLoading(false);
         }
-    }, [loading, setLoading, authenticated, setAuthenticated])
+    }, [setLoading, setAuthenticated])
 
     return (
         <>
diff --git a/wahlfang_web/src/pages/SpectatorView.js b/wahlfang_web/src/pages/SpectatorView.js
index e97ffd23c48c41e1615c35dc68cb9741142243a9..228dd93266a3416cc56a5196b918151afb3ac722 100644
--- a/wahlfang_web/src/pages/SpectatorView.js
+++ b/wahlfang_web/src/pages/SpectatorView.js
@@ -1,5 +1,120 @@
+import Layout from "../components/Layout";
+import React, {useEffect, useState} from "react";
+import {useParams} from "react-router-dom";
+import {fetchSpectatorInfo} from "../api";
+import Loading from "../components/Loading";
+import SpectatorElection from "../components/SpectatorElection";
+import Header from "../components/Header";
 
 
-export default function SpectatorView () {
+export default function SpectatorView() {
+    const [loading, setLoading] = useState(true);
+    const [error, setError] = useState(null);
+    const [data, setData] = useState({});
 
+    // TODO: error handling, reloading
+
+    const {uuid} = useParams();
+
+    useEffect(() => {
+        fetchSpectatorInfo(uuid)
+            .then((result) => {
+                setData(result);
+                setLoading(false)
+            })
+            .catch((err) => {
+                setError(err.toString());
+                setLoading(false);
+            })
+    }, [uuid, setLoading, setData, setError])
+
+    if (loading || error !== null) {
+        return (
+            <div id="content">
+                <Header/>
+                <Layout>
+                    {loading ? (
+                        <Loading/>
+                    ) : (
+                        <div className="alert alert-danger">{error}</div>
+                    )}
+                </Layout>
+            </div>
+        )
+    }
+
+    const elections = data.elections;
+    const open = elections.filter((election) => election.start_date !== null && new Date(election.start_date) <= new Date() && (election.end_date === null || new Date(election.end_date) > new Date()));
+    const upcoming = elections.filter((election) => election.start_date === null || new Date(election.start_date) > new Date());
+    const closed = elections.filter((election) => election.end_date && new Date(election.end_date) <= new Date());
+    const unpublished = closed.filter((election) => election.result_published === '0');
+    const published = closed.filter((election) => election.result_published === '1');
+
+    return (
+        <div id="content">
+            <Header/>
+            <Layout>
+                <div className="card bg-dark text-light shadow mb-2 py-2">
+                    <div className="card-body">
+                        <h4 className="text-center d-inline">{data.title}</h4>
+                        {data.meeting_link !== null ? (
+                            <div><small>Meeting at <a
+                                href={data.meeting_link}>{data.meeting_link}</a></small>
+                            </div>
+                        ) : ""}
+                    </div>
+                </div>
+                <div id="electionCard">
+                    {open.length > 0 ? (
+                        <div className="card shadow mb-2">
+                            <div className="card-header">
+                                <h4>Open Elections</h4>
+                            </div>
+                            <div className="card-body">
+                                {open.map(election => (
+                                    <SpectatorElection key={election.id} election={election}/>
+                                ))}
+                            </div>
+                        </div>
+                    ) : ""}
+                    {upcoming.length > 0 ? (
+                        <div className="card shadow mb-2">
+                            <div className="card-header">
+                                <h4>Upcoming Elections</h4>
+                            </div>
+                            <div className="card-body">
+                                {upcoming.map(election => (
+                                    <SpectatorElection key={election.id} election={election}/>
+                                ))}
+                            </div>
+                        </div>
+                    ) : ""}
+                    {unpublished.length > 0 ? (
+                        <div className="card shadow mb-2">
+                            <div className="card-header">
+                                <h4>Closed Elections</h4>
+                            </div>
+                            <div className="card-body">
+                                {unpublished.map(election => (
+                                    <SpectatorElection key={election.id} election={election}/>
+                                ))}
+                            </div>
+                        </div>
+                    ) : ""}
+                    {published.length > 0 ? (
+                        <div className="card shadow mb-2">
+                            <div className="card-header">
+                                <h4>Published Results</h4>
+                            </div>
+                            <div className="card-body">
+                                {published.map(election => (
+                                    <SpectatorElection key={election.id} election={election}/>
+                                ))}
+                            </div>
+                        </div>
+                    ) : ""}
+                </div>
+            </Layout>
+        </div>
+    )
 }
\ No newline at end of file
diff --git a/wahlfang_web/src/pages/VoteApp.js b/wahlfang_web/src/pages/VoteApp.js
index 3e08d955c5e28a4d5db0318016cca005a40e1ebe..aee0e6fded2c8ea552027e1b1199071847a502f5 100644
--- a/wahlfang_web/src/pages/VoteApp.js
+++ b/wahlfang_web/src/pages/VoteApp.js
@@ -21,31 +21,27 @@ export default function VoteApp() {
     const [loading, setLoading] = useState(!authenticated);
 
     useEffect(() => {
-        if (loading && !authenticated) {
-            const authToken = loadVoterToken();
-            if (authToken && isTokenValid(authToken.access)) {
-                setAuthenticated(true);
-                setLoading(false);
-                voterWS.initWs();
-                console.log("found valid access token");
-            } else if (authToken && isTokenValid(authToken.refresh)) {
-                console.log("found valid refresh token");
-                refreshVoterToken()
-                    .then(() => {
-                        setAuthenticated(true);
-                        setLoading(false);
-                        voterWS.initWs();
-                    })
-                    .catch(() => {
-                        setLoading(false);
-                    })
-            } else {
-                setLoading(false);
-            }
+        const authToken = loadVoterToken();
+        if (authToken && isTokenValid(authToken.access)) {
+            setAuthenticated(true);
+            setLoading(false);
+            voterWS.initWs();
+            console.log("found valid access token");
+        } else if (authToken && isTokenValid(authToken.refresh)) {
+            console.log("found valid refresh token");
+            refreshVoterToken()
+                .then(() => {
+                    setAuthenticated(true);
+                    setLoading(false);
+                    voterWS.initWs();
+                })
+                .catch(() => {
+                    setLoading(false);
+                })
+        } else {
+            setLoading(false);
         }
-    }, [loading, setLoading, authenticated, setAuthenticated])
-
-    console.log("vote app loading:", loading);
+    }, [setLoading, setAuthenticated])
 
     return (
         <>