diff --git a/cypress.config.js b/cypress.config.js
index 57c32a3d7c9..86edfbe5024 100644
--- a/cypress.config.js
+++ b/cypress.config.js
@@ -28,7 +28,6 @@ module.exports = defineConfig({
     supportFile: 'test/cypress/support/index.js',
   },
   retries: {
-    runMode: 2, // number of retries when running `cypress run`
-    openMode: 2, // number of retries when running `cypress open`
+    runMode: 2,
   },
 })
diff --git a/src/apps/companies/apps/referrals/send-referral/controllers.js b/src/apps/companies/apps/referrals/send-referral/controllers.js
index 41048158ccb..094385424c5 100644
--- a/src/apps/companies/apps/referrals/send-referral/controllers.js
+++ b/src/apps/companies/apps/referrals/send-referral/controllers.js
@@ -26,7 +26,9 @@ function renderSendReferralForm(req, res) {
         companyId: id,
         cancelUrl: urls.companies.detail(id),
         sendingAdviserTeamName,
-        flashMessages: res.locals.getMessages(),
+
+        // TODO: This should not be necessary as flashMessages are included in global props
+        // flashMessages: res.locals.flashMessages,
       },
     }
   )
diff --git a/src/apps/events/__test__/repos.test.js b/src/apps/events/__test__/repos.test.js
index 9b59f2fac7a..e2215bcbe40 100644
--- a/src/apps/events/__test__/repos.test.js
+++ b/src/apps/events/__test__/repos.test.js
@@ -5,15 +5,12 @@ const proxyquire = require('proxyquire')
 const authorisedRequestStub = sinon.stub()
 const searchStub = sinon.stub()
 
-const { saveEvent, fetchEvent, getAllEvents, getActiveEvents } = proxyquire(
-  '../repos',
-  {
-    '../../lib/authorised-request': {
-      authorisedRequest: authorisedRequestStub,
-    },
-    '../../modules/search/services': { search: searchStub },
-  }
-)
+const { saveEvent, getActiveEvents } = proxyquire('../repos', {
+  '../../lib/authorised-request': {
+    authorisedRequest: authorisedRequestStub,
+  },
+  '../../modules/search/services': { search: searchStub },
+})
 
 const config = require('../../../config')
 
@@ -50,27 +47,6 @@ describe('Event Service', () => {
     })
   })
 
-  context('fetchEvent', () => {
-    it('should fetch single event by id', async () => {
-      const id = '123'
-      await fetchEvent(mockReq, id)
-      expect(authorisedRequestStub).to.have.been.calledWith(
-        mockReq,
-        `${config.apiRoot}/v3/event/${id}`
-      )
-    })
-  })
-
-  context('getAllEvents', () => {
-    it('should fetch all events with limit and offset', async () => {
-      await getAllEvents(mockReq)
-      expect(authorisedRequestStub).to.have.been.calledWith(
-        mockReq,
-        `${config.apiRoot}/v3/event?limit=100000&offset=0`
-      )
-    })
-  })
-
   context('getActiveEvents', () => {
     let clock
 
diff --git a/src/apps/events/attendees/controllers/list.js b/src/apps/events/attendees/controllers/list.js
index 4b757abaea5..37a258ad987 100644
--- a/src/apps/events/attendees/controllers/list.js
+++ b/src/apps/events/attendees/controllers/list.js
@@ -48,6 +48,7 @@ async function renderAttendees(req, res, next) {
       children: [{ value: sortby }],
     })
 
+    // TODO: Can we remove this?
     res.breadcrumb(name).render('events/attendees/views/list', {
       incompleteEvent,
       attendees: attendeesCollection,
diff --git a/src/apps/events/attendees/router.js b/src/apps/events/attendees/router.js
index f00d3dd3dc0..7a592168bce 100644
--- a/src/apps/events/attendees/router.js
+++ b/src/apps/events/attendees/router.js
@@ -1,8 +1,9 @@
+// TODO: Remove this whole module and the whole parent folder ../
 const router = require('express').Router()
 
 const { createAttendee, renderAttendees } = require('./controllers')
 
-router.get('/', renderAttendees)
+router.get('/*', renderAttendees)
 
 router.get('/create/:contactId', createAttendee)
 
diff --git a/src/apps/events/middleware/details.js b/src/apps/events/middleware/details.js
index 81d2cae30aa..546022e8a4d 100644
--- a/src/apps/events/middleware/details.js
+++ b/src/apps/events/middleware/details.js
@@ -1,3 +1,4 @@
+// TODO: Get rid of this whole module
 const { assign } = require('lodash')
 
 const { transformEventFormBodyToApiRequest } = require('../transformers')
@@ -27,6 +28,7 @@ async function postDetails(req, res, next) {
   }
 }
 
+// TODO: Get rid of this
 async function getEventDetails(req, res, next, eventId) {
   try {
     res.locals.event = await fetchEvent(req, eventId)
diff --git a/src/apps/events/router.js b/src/apps/events/router.js
index 3d996ed1731..bccdb73225a 100644
--- a/src/apps/events/router.js
+++ b/src/apps/events/router.js
@@ -5,7 +5,7 @@ const { APP_PERMISSIONS, LOCAL_NAV } = require('./constants')
 const { handleRoutePermissions, setLocalNav } = require('../middleware')
 const { getEventDetails } = require('./middleware/details')
 const { renderEventsView } = require('./controllers/events')
-const attendeesRouter = require('./attendees/router')
+const { createAttendee } = require('./attendees/controllers/create')
 
 router.get('/create', renderEventsView)
 router.use(handleRoutePermissions(APP_PERMISSIONS))
@@ -19,12 +19,9 @@ router.use(
   setLocalNav(LOCAL_NAV)
 )
 
-router.use('/:eventId/attendees', attendeesRouter)
+// TODO: Get rid of this and fetch the event on the client
 router.param('eventId', getEventDetails)
-// TODO: When everything in the events space is converted to react
-// router.get('/*', renderEventsView)
-router.get('/:eventId/edit', renderEventsView)
-router.get('/:eventId', renderEventsView)
-router.get('/:eventId/details', renderEventsView)
+router.get('/:eventId/attendees/create/:contactId', createAttendee)
+router.get('/:eventId*', renderEventsView)
 
 module.exports = router
diff --git a/src/client/components/Resource/InteractionsV3.js b/src/client/components/Resource/InteractionsV3.js
new file mode 100644
index 00000000000..99bef7fb5f1
--- /dev/null
+++ b/src/client/components/Resource/InteractionsV3.js
@@ -0,0 +1,3 @@
+import { createCollectionResource } from './Resource'
+
+export default createCollectionResource('InteractionsV3', 'v3/interaction')
diff --git a/src/client/components/Resource/Paginated.js b/src/client/components/Resource/Paginated.js
index 97b01fb8bdd..6c3608db5ae 100644
--- a/src/client/components/Resource/Paginated.js
+++ b/src/client/components/Resource/Paginated.js
@@ -53,6 +53,9 @@ const StyledCollectionSort = styled(CollectionSort)`
  * Forwarded to {qsParamName} prop of {StyledCollectionSort}.
  * @param {number} [props.defaultSortOptionIndex = 0] - The index of the sort option
  * that should be selected when there's nothing set in the query string.
+ * @param {string} [props.addItemUrl =] - If set, an "Add <item-name>" button
+ * will be displayed on the top-right corner of the header behaving as a link
+ * to the value of this prop. This is just forwarded to CollectionHeader.
  * @example
  * <PaginatedResource name="My task name" id="foo">
  *   {currentPage =>
@@ -100,6 +103,7 @@ const PaginatedResource = multiInstance({
     result,
     shouldPluralize,
     noResults = "You don't have any results",
+    addItemUrl,
   }) => {
     // We know better than ESLint that we are in deed in a React component
     // eslint-disable-next-line react-hooks/rules-of-hooks
@@ -150,6 +154,7 @@ const PaginatedResource = multiInstance({
                     totalItems={result.count}
                     collectionName={heading || name}
                     shouldPluralize={shouldPluralize}
+                    addItemUrl={addItemUrl}
                   />
                   {totalPages > 0 && (
                     <StyledCollectionSort
diff --git a/src/client/components/Resource/tasks.js b/src/client/components/Resource/tasks.js
index bc28615076d..e602fa7a79a 100644
--- a/src/client/components/Resource/tasks.js
+++ b/src/client/components/Resource/tasks.js
@@ -6,6 +6,7 @@ import Company from './Company'
 import CompanyContacts from './CompanyContacts'
 import Countries from './Countries'
 import Interactions from './Interactions'
+import InteractionsV3 from './InteractionsV3'
 import Opportunity from './Opportunity'
 import OpportunityStatuses from './OpportunityStatuses'
 import UKRegions from './UKRegions'
@@ -109,6 +110,7 @@ export default {
   ...ContactAuditHistory.tasks,
   ...Countries.tasks,
   ...Interactions.tasks,
+  ...InteractionsV3.tasks,
   ...Opportunity.tasks,
   ...OpportunityStatuses.tasks,
   ...CapitalInvestmentRequiredChecksConducted.tasks,
diff --git a/src/client/modules/Events/CollectionList/index.jsx b/src/client/modules/Events/CollectionList/index.jsx
index 4bcf5814275..5fc23cc65b1 100644
--- a/src/client/modules/Events/CollectionList/index.jsx
+++ b/src/client/modules/Events/CollectionList/index.jsx
@@ -69,7 +69,7 @@ const EventTemplate = (item) => {
     <StyledCollectionItem
       dataTest="data-hub-event"
       headingText={item.headingText}
-      headingUrl={item.headingUrl}
+      headingUrl={`/events/${item.id}/details`}
       metadata={item.metadata}
       tags={item.tags}
       titleRenderer={TitleRenderer}
diff --git a/src/client/modules/Events/EventDetails/index.jsx b/src/client/modules/Events/EventDetails/index.jsx
index 99614daf268..4234d67c941 100644
--- a/src/client/modules/Events/EventDetails/index.jsx
+++ b/src/client/modules/Events/EventDetails/index.jsx
@@ -3,33 +3,101 @@ import { connect } from 'react-redux'
 import { isEmpty } from 'lodash'
 import { useParams } from 'react-router-dom'
 
-import GridRow from '@govuk-react/grid-row'
-import GridCol from '@govuk-react/grid-col'
 import Button from '@govuk-react/button'
 import styled from 'styled-components'
 
+import { H3, Link } from 'govuk-react'
+
 import urls from '../../../../lib/urls'
 import { TASK_GET_EVENT_DETAILS, ID, state2props } from './state'
 import { EVENTS__DETAILS_LOADED } from '../../../actions'
 import Task from '../../../components/Task'
-import {
-  LocalNav,
-  LocalNavLink,
-  SummaryTable,
-  FormActions,
-  DefaultLayout,
-} from '../../../components'
+import { SummaryTable, FormActions, DefaultLayout } from '../../../components'
+import CollectionItem from '../../../components/CollectionList/CollectionItem'
+import State from '../../../components/State'
+
+import { VerticalTabNav } from '../../../components/TabNav'
+import InteractionsV3 from '../../../components/Resource/InteractionsV3'
+import { formatDate, DATE_FORMAT_FULL } from '../../../utils/date-utils'
+import StatusMessage from '../../../components/StatusMessage'
 
 const StyledSummaryTable = styled(SummaryTable)({
   marginTop: 0,
 })
 
-const EventDetails = ({
-  name = 'Event',
+const Attendees = ({ eventId, isDisabled }) => (
+  <div>
+    <H3 as="h2">Event Attendees</H3>
+    {isDisabled && (
+      <StatusMessage>
+        You cannot add an event attendee because the event has been disabled.
+      </StatusMessage>
+    )}
+    <InteractionsV3.Paginated
+      id="???"
+      heading="attendee"
+      addItemUrl={!isDisabled && `/events/${eventId}/attendees/find-new`}
+      sortOptions={[
+        { name: 'Last name: A-Z', value: 'last_name_of_first_contact' },
+        { name: 'Last name: Z-A', value: '-last_name_of_first_contact' },
+        { name: 'First name: A-Z', value: 'first_name_of_first_contact' },
+        { name: 'First name: Z-A', value: '-first_name_of_first_contact' },
+        { name: 'Company name: A-Z', value: 'company__name' },
+        { name: 'Company name: Z-A', value: '-company__name' },
+        { name: 'Recently added', value: '-created_on' },
+        { name: 'Least recently added', value: 'created_on' },
+      ]}
+      payload={{
+        event_id: eventId,
+      }}
+    >
+      {(page) => (
+        <ul>
+          {page.map(
+            ({ contacts: [contact], companies: [company], date, service }) => (
+              <CollectionItem
+                headingText={contact?.name || 'Not available'}
+                headingUrl={contact && `/contacts/${contact?.id}`}
+                metadata={[
+                  {
+                    label: 'Company',
+                    value: (
+                      <Link href={`/companies/${company?.id}`}>
+                        {company?.name}
+                      </Link>
+                    ),
+                  },
+                  {
+                    label: 'Job title',
+                    value: contact?.job_title || 'Not available',
+                  },
+                  {
+                    label: 'Date attended',
+                    value: formatDate(date, DATE_FORMAT_FULL),
+                  },
+                  {
+                    label: 'Service delivery',
+                    value: (
+                      <Link href={`/companies/${service.id}`}>
+                        {service.name}
+                      </Link>
+                    ),
+                  },
+                ]}
+              />
+            )
+          )}
+        </ul>
+      )}
+    </InteractionsV3.Paginated>
+  </div>
+)
+
+const Details = ({
   eventType,
+  eventDays,
   startDate,
   endDate,
-  eventDays,
   locationType,
   fullAddress,
   ukRegion,
@@ -39,131 +107,139 @@ const EventDetails = ({
   otherTeams,
   relatedProgrammes,
   relatedTradeAgreements,
-  service,
   disabledOn,
-}) => {
-  const { id } = useParams()
-  const breadcrumbs = [
-    {
-      link: urls.dashboard.index(),
-      text: 'Home',
-    },
-    {
-      link: urls.events.index(),
-      text: 'Events',
-    },
-    {
-      text: name || '',
-    },
-  ]
+  service,
+  id,
+}) => (
+  <>
+    <StyledSummaryTable>
+      <SummaryTable.Row heading="Type of event" children={eventType} />
+      <SummaryTable.Row
+        heading={eventDays == 1 ? 'Event date' : 'Event start date'}
+        children={startDate}
+      />
+      {eventDays > 1 ? (
+        <SummaryTable.Row heading="Event end date" children={endDate} />
+      ) : null}
+      <SummaryTable.Row heading="Event location type">
+        {isEmpty(locationType) ? 'Not set' : locationType}
+      </SummaryTable.Row>
+      <SummaryTable.Row heading="Address">{fullAddress}</SummaryTable.Row>
+      <SummaryTable.Row heading="Region" hideWhenEmpty={false}>
+        {isEmpty(ukRegion) ? 'Not set' : ukRegion}
+      </SummaryTable.Row>
+      <SummaryTable.Row heading="Notes" hideWhenEmpty={false}>
+        {isEmpty(notes) ? 'Not set' : notes}
+      </SummaryTable.Row>
+      <SummaryTable.Row heading="Lead team">
+        {isEmpty(leadTeam) ? 'Not set' : leadTeam}
+      </SummaryTable.Row>
+      <SummaryTable.Row heading="Organiser">
+        {isEmpty(organiser) ? 'Not set' : organiser}
+      </SummaryTable.Row>
+      <SummaryTable.ListRow
+        heading="Other teams"
+        value={otherTeams}
+        emptyValue="Not set"
+        hideWhenEmpty={false}
+      />
+      <SummaryTable.ListRow
+        heading="Related programmes"
+        value={relatedProgrammes}
+        emptyValue="Not set"
+        hideWhenEmpty={false}
+      />
+      <SummaryTable.ListRow
+        heading="Related Trade Agreements"
+        value={relatedTradeAgreements}
+        emptyValue="Not set"
+        hideWhenEmpty={false}
+      />
+      <SummaryTable.Row heading="Service" children={service} />
+    </StyledSummaryTable>
+    {!disabledOn && (
+      <FormActions>
+        <Button as={'a'} href={urls.events.edit(id)}>
+          Edit event
+        </Button>
+      </FormActions>
+    )}
+  </>
+)
+
+const EventDetails = ({ name, ...props }) => {
+  const { id, ['*']: path } = useParams()
 
   return (
-    <DefaultLayout
-      heading="Events"
-      pageTitle="Events"
-      breadcrumbs={breadcrumbs}
-      useReactRouter={true}
-    >
-      <Task.Status
-        name={TASK_GET_EVENT_DETAILS}
-        id={ID}
-        progressMessage="loading event details"
-        startOnRender={{
-          payload: id,
-          onSuccessDispatch: EVENTS__DETAILS_LOADED,
-        }}
-      >
-        {() => {
-          return (
-            name && (
-              <GridRow data-test="eventDetails">
-                <GridCol setWidth="one-quarter">
-                  <LocalNav data-test="event-details-nav">
-                    <LocalNavLink
-                      data-test="event-details-nav-link"
-                      href={urls.events.details(id)}
-                    >
-                      Details
-                    </LocalNavLink>
-                    <LocalNavLink
-                      data-test="event-details-nav-link"
-                      href={urls.events.attendees(id)}
-                    >
-                      Attendees
-                    </LocalNavLink>
-                  </LocalNav>
-                </GridCol>
-                <GridCol setWidth="three-quarters">
-                  <StyledSummaryTable>
-                    <SummaryTable.Row
-                      heading="Type of event"
-                      children={eventType}
-                    />
-                    <SummaryTable.Row
-                      heading={
-                        eventDays == 1 ? 'Event date' : 'Event start date'
-                      }
-                      children={startDate}
-                    />
-                    {eventDays > 1 ? (
-                      <SummaryTable.Row
-                        heading="Event end date"
-                        children={endDate}
-                      />
-                    ) : null}
-                    <SummaryTable.Row heading="Event location type">
-                      {isEmpty(locationType) ? 'Not set' : locationType}
-                    </SummaryTable.Row>
-                    <SummaryTable.Row heading="Address">
-                      {fullAddress}
-                    </SummaryTable.Row>
-                    <SummaryTable.Row heading="Region" hideWhenEmpty={false}>
-                      {isEmpty(ukRegion) ? 'Not set' : ukRegion}
-                    </SummaryTable.Row>
-                    <SummaryTable.Row heading="Notes" hideWhenEmpty={false}>
-                      {isEmpty(notes) ? 'Not set' : notes}
-                    </SummaryTable.Row>
-                    <SummaryTable.Row heading="Lead team">
-                      {isEmpty(leadTeam) ? 'Not set' : leadTeam}
-                    </SummaryTable.Row>
-                    <SummaryTable.Row heading="Organiser">
-                      {isEmpty(organiser) ? 'Not set' : organiser}
-                    </SummaryTable.Row>
-                    <SummaryTable.ListRow
-                      heading="Other teams"
-                      value={otherTeams}
-                      emptyValue="Not set"
-                      hideWhenEmpty={false}
-                    />
-                    <SummaryTable.ListRow
-                      heading="Related programmes"
-                      value={relatedProgrammes}
-                      emptyValue="Not set"
-                      hideWhenEmpty={false}
-                    />
-                    <SummaryTable.ListRow
-                      heading="Related Trade Agreements"
-                      value={relatedTradeAgreements}
-                      emptyValue="Not set"
-                      hideWhenEmpty={false}
-                    />
-                    <SummaryTable.Row heading="Service" children={service} />
-                  </StyledSummaryTable>
-                  {!disabledOn && (
-                    <FormActions>
-                      <Button as={'a'} href={urls.events.edit(id)}>
-                        Edit event
-                      </Button>
-                    </FormActions>
-                  )}
-                </GridCol>
-              </GridRow>
-            )
-          )
-        }}
-      </Task.Status>
-    </DefaultLayout>
+    <State>
+      {({ flashMessages }) => (
+        <DefaultLayout
+          heading={name}
+          pageTitle="Events"
+          flashMessages={{
+            ...flashMessages,
+            info: [
+              ...(flashMessages.info || []),
+              ...(props.disabledOn
+                ? [
+                    `This event was disabled on ${formatDate(props.disabledOn, DATE_FORMAT_FULL)} and can no longer be edited.`,
+                  ]
+                : []),
+            ],
+          }}
+          breadcrumbs={[
+            {
+              link: urls.dashboard.index(),
+              text: 'Home',
+            },
+            {
+              link: urls.events.index(),
+              text: 'Events',
+            },
+            {
+              text: name,
+            },
+            {
+              text: { details: 'Details', attendees: 'Attendees' }[path],
+            },
+          ]}
+          useReactRouter={true}
+        >
+          <Task.Status
+            name={TASK_GET_EVENT_DETAILS}
+            id={ID}
+            progressMessage="loading event details"
+            startOnRender={{
+              payload: id,
+              onSuccessDispatch: EVENTS__DETAILS_LOADED,
+            }}
+          >
+            {() => (
+              <VerticalTabNav
+                routed={true}
+                id="event-details-tab-nav"
+                label="Event tab navigation"
+                selectedIndex="attendees"
+                tabs={{
+                  [`/events/${id}/details`]: {
+                    label: 'Details',
+                    content: <Details {...props} />,
+                  },
+                  [`/events/${id}/attendees`]: {
+                    label: 'Attendees',
+                    content: (
+                      <Attendees eventId={id} isDisabled={props.disabledOn} />
+                    ),
+                  },
+                }}
+              />
+            )}
+          </Task.Status>
+        </DefaultLayout>
+      )}
+    </State>
   )
 }
 
+// TODO: Get rid of this
 export default connect(state2props)(EventDetails)
diff --git a/src/client/routes.js b/src/client/routes.js
index 56408ae63e7..95944ffc67e 100644
--- a/src/client/routes.js
+++ b/src/client/routes.js
@@ -400,7 +400,7 @@ function Routes() {
       ),
     },
     {
-      path: '/events/:id/details',
+      path: '/events/:id/*',
       element: (
         <ProtectedRoute module={'datahub:events'}>
           <EventDetails />
diff --git a/src/middleware/__test__/user-locals.test.js b/src/middleware/__test__/user-locals.test.js
index 278858b1cc3..ace496818ec 100644
--- a/src/middleware/__test__/user-locals.test.js
+++ b/src/middleware/__test__/user-locals.test.js
@@ -1,93 +1,29 @@
-const proxyquire = require('proxyquire')
-const { faker } = require('@faker-js/faker')
-
-const words = faker.lorem.words
-const modulePath = '../user-locals'
-
-describe('user-locals middleware', () => {
-  let req, res, next
-  beforeEach(() => {
-    res = {
-      locals: {},
+const { parseFlashMessages } = require('../user-locals')
+
+describe('parseFlashMessages', () => {
+  it('valid JSON in success:with-body', () => {
+    const BODY = { heading: 'foo bar', body: 'baz bing' }
+    const RAW_FLASH_MESSAGES = {
+      success: ['foo', '{"test":1}'],
+      error: ['bar', 'baz'],
+      'success:with-body': [JSON.stringify(BODY)],
     }
-    next = sinon.spy()
-  })
 
-  describe('#getMessages', () => {
-    afterEach(() => {
-      expect(next).to.have.been.called
+    expect(parseFlashMessages(RAW_FLASH_MESSAGES)).to.deep.equal({
+      ...RAW_FLASH_MESSAGES,
+      'success:with-body': [BODY],
     })
+  })
 
-    context('With no messages in the flash', () => {
-      it('returns the values', () => {
-        const userLocals = require(modulePath)
-        const flash = sinon.stub().returns({})
-        req = {
-          path: '',
-          flash,
-        }
-        userLocals(req, res, next)
-
-        const messages = res.locals.getMessages()
-
-        expect(flash).to.have.been.calledWith()
-        expect(messages).to.deep.equal({})
-      })
-    })
-
-    context('With valid messages in the flash', () => {
-      it('returns the values', () => {
-        const userLocals = require(modulePath)
-        const flashData = {
-          success: [words(5), '{"test":1}'],
-          error: [words(5), words(5)],
-          'success:with-body': [
-            JSON.stringify({ heading: words(2), body: words(10) }),
-          ],
-        }
-        const flash = sinon.stub().returns(flashData)
-        req = {
-          path: '',
-          flash,
-        }
-
-        userLocals(req, res, next)
-
-        const messages = res.locals.getMessages()
-
-        expect(messages).to.equal(flashData)
-        expect(typeof messages['success:with-body'][0]).to.equal('object')
-        expect(typeof messages.success[1]).to.equal('string')
-        expect(flash).to.have.been.calledWith()
-      })
-    })
-
-    context('With invalid JSON messages in the flash', () => {
-      it('returns the string and reports the error', () => {
-        const captureException = sinon.spy()
-        const userLocals = proxyquire(modulePath, {
-          '../lib/reporter': {
-            captureException,
-          },
-        })
-        const flashData = {
-          'success:with-body': [`heading: ${words(2)}`],
-        }
-        const flash = sinon.stub().returns(flashData)
-        req = {
-          path: '',
-          flash,
-        }
-
-        userLocals(req, res, next)
-
-        const messages = res.locals.getMessages()
+  it('invalid JSON in success:with-body', () => {
+    const RAW_FLASH_MESSAGES = {
+      success: ['foo', '{"test":1}'],
+      error: ['bar', 'baz'],
+      'success:with-body': ["I'm no valid JSON"],
+    }
 
-        expect(messages).to.equal(flashData)
-        expect(typeof messages['success:with-body'][0]).to.equal('string')
-        expect(flash).to.have.been.calledWith()
-        expect(captureException).to.have.been.called
-      })
-    })
+    expect(parseFlashMessages(RAW_FLASH_MESSAGES)).to.deep.equal(
+      RAW_FLASH_MESSAGES
+    )
   })
 })
diff --git a/src/middleware/react-global-props.js b/src/middleware/react-global-props.js
index b87abe16727..c6737c35bdb 100644
--- a/src/middleware/react-global-props.js
+++ b/src/middleware/react-global-props.js
@@ -9,6 +9,7 @@ const transformModulePermissions = (modules) =>
 module.exports = () => {
   return function reactGlobalProps(req, res, next) {
     res.locals.globalProps = {
+      flashMessages: res.locals.flashMessages,
       sentryDsn: config.sentryDsn,
       sentryEnvironment: config.sentryEnvironment,
       csrfToken: req.csrfToken(),
diff --git a/src/middleware/user-locals.js b/src/middleware/user-locals.js
index ec46d8ce1ed..f9ba51c4388 100644
--- a/src/middleware/user-locals.js
+++ b/src/middleware/user-locals.js
@@ -27,7 +27,15 @@ function convertValueToJson(value) {
   }
 }
 
-module.exports = (req, res, next) => {
+const parseFlashMessages = (rawFlashMessages) =>
+  Object.fromEntries(
+    Object.entries(rawFlashMessages).map(([k, v]) => [
+      k,
+      k.endsWith(':with-body') ? v.map(convertValueToJson) : v,
+    ])
+  )
+
+const userLocals = (req, res, next) => {
   const userPermissions = get(res, 'locals.user.permissions')
   const userProfile = config.oauth.bypassSSO
     ? null
@@ -37,28 +45,22 @@ module.exports = (req, res, next) => {
     filterNonPermittedItem(userPermissions)
   )
 
-  Object.assign(res.locals, {
-    PERMITTED_APPLICATIONS: config.oauth.bypassSSO
-      ? [{ key: 'datahub-crm' }]
-      : permittedApplications,
-    ALLOWED_APPS: permittedNavItems.reduce((apps, { headerKey }) => {
-      headerKey && apps.push(headerKey)
-      return apps
-    }, []),
-    ACTIVE_KEY: getActiveHeaderKey(req.path, permittedNavItems),
-
-    getMessages() {
-      const items = req.flash()
+  res.locals.PERMITTED_APPLICATIONS = config.oauth.bypassSSO
+    ? [{ key: 'datahub-crm' }]
+    : permittedApplications
 
-      for (const [key, values] of Object.entries(items)) {
-        if (key.endsWith(':with-body')) {
-          items[key] = values.map(convertValueToJson)
-        }
-      }
+  res.locals.ALLOWED_APPS = permittedNavItems.reduce((apps, { headerKey }) => {
+    headerKey && apps.push(headerKey)
+    return apps
+  }, [])
 
-      return items
-    },
-  })
+  res.locals.ACTIVE_KEY = getActiveHeaderKey(req.path, permittedNavItems)
+  res.locals.flashMessages = parseFlashMessages(req.flash())
 
   next()
 }
+
+module.exports = {
+  parseFlashMessages,
+  userLocals,
+}
diff --git a/src/server.js b/src/server.js
index 4c161763695..0962944400c 100644
--- a/src/server.js
+++ b/src/server.js
@@ -20,7 +20,7 @@ const currentJourney = require('./modules/form/current-journey')
 const nunjucks = require('./config/nunjucks')
 const headers = require('./middleware/headers')
 const locals = require('./middleware/locals')
-const userLocals = require('./middleware/user-locals')
+const { userLocals } = require('./middleware/user-locals')
 const user = require('./middleware/user')
 const auth = require('./middleware/auth')
 const store = require('./middleware/store')
diff --git a/src/templates/_macros/common/local-header.njk b/src/templates/_macros/common/local-header.njk
index 15517e18401..437d64c621f 100644
--- a/src/templates/_macros/common/local-header.njk
+++ b/src/templates/_macros/common/local-header.njk
@@ -14,7 +14,7 @@
  #}
 {% macro LocalHeader(props) %}
   {% set breadcrumbs = props.breadcrumbs | default(getBreadcrumbs()) %}
-  {% set messages = props.messages | default(getMessages() if getMessages) %}
+  {% set messages = props.messages | default(flashMessages) %}
   {% set modifier = props.modifier | concat('') | reverse | join(' c-local-header--') if props.modifier %}
 
   {% if props %}
diff --git a/test/end-to-end/cypress/specs/DIT/event-spec.js b/test/end-to-end/cypress/specs/DIT/event-spec.js
index bd1a507a119..eb6e3caf2f5 100644
--- a/test/end-to-end/cypress/specs/DIT/event-spec.js
+++ b/test/end-to-end/cypress/specs/DIT/event-spec.js
@@ -123,22 +123,15 @@ describe('Event', () => {
       cy.visit(urls.events.index())
       cy.contains(eventName).click()
       cy.contains('Attendees').click()
-      cy.get(selectors.entityCollection.addAttendee).click()
+      cy.contains('Add attendee').click()
 
-      cy.get('[data-test="contact-name-filter"]')
-        .type('dean cox')
-        .type('{enter}')
+      cy.get('input[name="name"]').type('dean cox').type('{enter}')
       cy.contains('Dean Cox').click()
 
-      cy.get(selectors.message.flashMessages)
-        .should(
-          'contain',
-          'Event attendee added - This has created a service delivery record.'
-        )
-        .and(
-          'contain',
+      cy.contains(
+        'Event attendee added - This has created a service delivery record. ' +
           'If required, you can view or edit the service delivery directly from the attendee record.'
-        )
+      )
     })
   })
 
@@ -159,7 +152,7 @@ describe('Event', () => {
         .invoke('text')
         .should('contain', 'Account management')
       cy.contains(eventName).click()
-      cy.get(selectors.entityCollection.editEvent).click()
+      cy.contains('a', 'Edit event')
       fillEventType('Exhibition')
       clickSaveAndReturnButton()
 
@@ -182,9 +175,7 @@ describe('Event', () => {
       cy.visit(urls.events.details(event.pk))
       cy.contains('a', 'Attendees').click()
       cy.contains('Add attendee').click()
-      cy.get('[data-test="contact-name-filter"]')
-        .type('Attendee')
-        .type('{enter}')
+      cy.get('input[name="name"]').type('Attendee').type('{enter}')
       cy.contains('Joe Attendee').click()
       cy.contains('Event attendee added')
     })
@@ -193,17 +184,12 @@ describe('Event', () => {
       cy.visit(urls.events.details(event.pk))
       cy.contains('a', 'Attendees').click()
       cy.contains('Add attendee').click()
-      cy.get('[data-test="contact-name-filter"]')
-        .type('Attendee')
-        .type('{enter}')
+      cy.get('input[name="name"]').type('Attendee').type('{enter}')
       cy.contains('Joe Attendee').click()
       cy.contains('Add attendee').click()
-      cy.get('[data-test="contact-name-filter"]')
-        .type('Attendee')
-        .type('{enter}')
+      cy.get('input[name="name"]').type('Attendee').type('{enter}')
       cy.contains('Joe Attendee').click()
-      cy.get(selectors.message.flashMessages).should(
-        'contain',
+      cy.contains(
         'Event attendee not added - This contact has already been added as an event attendee'
       )
     })
diff --git a/test/end-to-end/cypress/specs/DIT/local-nav-spec.js b/test/end-to-end/cypress/specs/DIT/local-nav-spec.js
index 0c14e265066..883ec59d719 100644
--- a/test/end-to-end/cypress/specs/DIT/local-nav-spec.js
+++ b/test/end-to-end/cypress/specs/DIT/local-nav-spec.js
@@ -108,17 +108,4 @@ describe('DBT Permission', () => {
       ])
     })
   })
-
-  describe('event', () => {
-    beforeEach(() => {
-      const event = fixtures.event.create.defaultEvent()
-      cy.loadFixture([event])
-      cy.visit(urls.events.details(event.pk))
-    })
-
-    it('should display DBT only side navs', () => {
-      const navSelector = '[data-test="event-details-nav-link"]'
-      assertLocalNav(navSelector, ['Details', 'Attendee'])
-    })
-  })
 })
diff --git a/test/functional/cypress/specs/events/attendees-search-spec.js b/test/functional/cypress/specs/events/attendees-search-spec.js
index 012730c3854..03f9904e16e 100644
--- a/test/functional/cypress/specs/events/attendees-search-spec.js
+++ b/test/functional/cypress/specs/events/attendees-search-spec.js
@@ -174,12 +174,9 @@ describe('Event attendee search', () => {
     it('should add an attendee to the event', () => {
       cy.contains('Hanna Reinger').click()
 
-      cy.get('.c-message')
-        .should('exist')
-        .should(
-          'have.text',
-          'Event attendee added - This has created a service delivery record. If required, you can view or edit the service delivery directly from the attendee record.Dismiss'
-        )
+      cy.contains(
+        'Event attendee added - This has created a service delivery record. If required, you can view or edit the service delivery directly from the attendee record.'
+      )
     })
   })
 })
diff --git a/test/functional/cypress/specs/events/attendees-spec.js b/test/functional/cypress/specs/events/attendees-spec.js
index 240d26608ef..992b74e5d60 100644
--- a/test/functional/cypress/specs/events/attendees-spec.js
+++ b/test/functional/cypress/specs/events/attendees-spec.js
@@ -1,5 +1,4 @@
 const fixtures = require('../../fixtures')
-const selectors = require('../../../../selectors')
 const urls = require('../../../../../src/lib/urls')
 
 const {
@@ -8,101 +7,35 @@ const {
 } = require('../../support/assertions')
 
 describe('Event Attendees', () => {
-  context('Enabled events', () => {
-    beforeEach(() => {
-      cy.visit(`/events/${fixtures.event.oneDayExhibition.id}/attendees`)
-      cy.get('.c-collection').as('collectionList')
-    })
-
-    it('should render breadcrumbs', () => {
-      assertBreadcrumbs({
-        Home: urls.dashboard.index(),
-        Events: urls.events.index(),
-        'One-day exhibition': null,
-      })
-    })
-
-    it('should render the header', () => {
-      assertLocalHeader('One-day exhibition')
-    })
-
-    it('should display a collection header', () => {
-      cy.get('main article h2').should('contain', 'Event Attendees')
-    })
-
-    it('should display attendee collection list', () => {
-      cy.get(selectors.entityCollection.collection)
-        .find('header')
-        .should('contain', '1,233')
-        .parent()
-        .find('span')
-        .should('contain', 'Page 1 of 13')
-        .parents('header')
-        .next()
-        .find('li')
-        .should('have.length', 100)
-    })
-
-    it('should be able to use the pagination', () => {
-      cy.get(selectors.entityCollection.collection)
-        .find('nav ul li')
-        .as('pageLinks')
-        .eq(1)
-        .click()
-
-      cy.get(selectors.entityCollection.collection)
-        .find('header')
-        .should('contain', 'Page 2 of 13')
-
-      cy.get('@pageLinks').eq(0).click()
-
-      cy.get(selectors.entityCollection.collection)
-        .find('header')
-        .should('contain', 'Page 1 of 13')
-    })
+  it('Enabled events', () => {
+    cy.visit(`/events/${fixtures.event.oneDayExhibition.id}/attendees`)
+    assertBreadcrumbs({
+      Home: urls.dashboard.index(),
+      Events: urls.events.index(),
+      'One-day exhibition': null,
+      Attendees: null,
+    })
+    assertLocalHeader('One-day exhibition')
+    cy.contains('h2', 'Event Attendees')
+    cy.contains('h2', '1,233 attendees')
+    cy.contains('Page 1 of 124')
   })
-  context('Disabled events', () => {
-    beforeEach(() => {
-      cy.visit(`/events/${fixtures.event.teddyBearExpo.id}/attendees`)
-    })
-
-    it('should render breadcrumbs', () => {
-      assertBreadcrumbs({
-        Home: urls.dashboard.index(),
-        Events: urls.events.index(),
-        'Teddy bear expo': null,
-      })
-    })
-
-    it('should render the header', () => {
-      assertLocalHeader('Teddy bear expo')
-    })
-
-    it('should display a message indicating when the event was disabled', () => {
-      assertLocalHeader(
-        'This event was disabled on 5 September 2017 and can no longer be edited'
-      )
-    })
 
-    it('should not display add attendees button for disabled events', () => {
-      cy.get(selectors.entityCollection.addAttendee).should('not.exist')
-    })
-
-    it('should display a message indicating that you cannot add an event attendee', () => {
-      cy.get('main article h2')
-        .next()
-        .should(
-          'contain',
-          'You cannot add an event attendee because the event has been disabled.'
-        )
-    })
-
-    it('should display a collection header', () => {
-      cy.get('main article h2').should('contain', 'Event Attendees')
-    })
-
-    it('should display attendee collection list', () => {
-      cy.get(selectors.collection.headerCount).should('contain', '1,233')
-    })
+  it('Disabled events', () => {
+    cy.visit(`/events/${fixtures.event.teddyBearExpo.id}/attendees`)
+    assertBreadcrumbs({
+      Home: urls.dashboard.index(),
+      Events: urls.events.index(),
+      'Teddy bear expo': null,
+      Attendees: null,
+    })
+    assertLocalHeader('Teddy bear expo')
+    assertLocalHeader(
+      'This event was disabled on 5 September 2017 and can no longer be edited'
+    )
+    cy.contains(
+      'You cannot add an event attendee because the event has been disabled.'
+    )
+    cy.contains('Add attendee').should('not.exist')
   })
 })
diff --git a/test/functional/cypress/specs/events/details-spec.js b/test/functional/cypress/specs/events/details-spec.js
index 673d253ccc6..9dd1fddc70f 100644
--- a/test/functional/cypress/specs/events/details-spec.js
+++ b/test/functional/cypress/specs/events/details-spec.js
@@ -1,141 +1,120 @@
 import urls from '../../../../../src/lib/urls'
 
 const {
-  assertKeyValueTable,
   assertBreadcrumbs,
+  assertSummaryTableStrict,
 } = require('../../support/assertions')
 
 const fixtures = require('../../fixtures')
 const event = require('../../../../sandbox/fixtures/v3/event/single-event-missing-teams.json')
-const selectors = require('../../../../selectors')
+
+const EVENT_DETAILS_ROWS = {
+  'Type of event': 'Exhibition',
+  'Event date': '1 January 2021',
+  'Event location type': 'HQ',
+  Address: 'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China',
+  Region: 'Not set',
+  Notes: 'This is a dummy event for testing.',
+  'Lead team': 'CBBC Hangzhou',
+  Organiser: 'John Rogers',
+  'Other teams': 'CBBC HangzhouCBBC North West',
+  'Related programmes': 'Grown in Britain',
+  'Related Trade Agreements': 'UK - Japan',
+  Service: 'Events : UK based',
+}
 
 describe('Event Details', () => {
   it('should display one day event details', () => {
     cy.visit(urls.events.details(fixtures.event.oneDayExhibition.id))
 
-    cy.get(selectors.entityCollection.editEvent).should('be.visible')
-
     assertBreadcrumbs({
       Home: urls.dashboard.index.route,
       Events: urls.events.index(),
       'One-day exhibition': null,
+      Details: null,
     })
 
-    assertKeyValueTable('eventDetails', {
-      'Type of event': 'Exhibition',
-      'Event date': '1 January 2021',
-      'Event location type': 'HQ',
-      Address:
-        'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China',
-      Region: 'Not set',
-      Notes: 'This is a dummy event for testing.',
-      'Lead team': 'CBBC Hangzhou',
-      Organiser: 'John Rogers',
-      'Other teams': 'CBBC HangzhouCBBC North West',
-      'Related programmes': 'Grown in Britain',
-      'Related Trade Agreements': 'UK - Japan',
-      Service: 'Events : UK based',
+    assertSummaryTableStrict({
+      rows: EVENT_DETAILS_ROWS,
     })
+
+    cy.contains('a', 'Edit event')
   })
 
   it('should display one day event details with no teams', () => {
     cy.visit(urls.events.details(event.id))
 
-    cy.get(selectors.entityCollection.editEvent).should('be.visible')
-
-    assertKeyValueTable('eventDetails', {
-      'Type of event': 'Exhibition',
-      'Event date': '1 January 2021',
-      'Event location type': 'HQ',
-      Address:
-        'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China',
-      Region: 'Not set',
-      Notes: 'This is a dummy event for testing.',
-      'Lead team': 'Not set',
-      Organiser: 'John Rogers',
-      'Other teams': 'Not set',
-      'Related programmes': 'Grown in Britain',
-      'Related Trade Agreements': 'UK - Japan',
-      Service: 'Events : UK based',
+    assertSummaryTableStrict({
+      rows: {
+        ...EVENT_DETAILS_ROWS,
+        'Lead team': 'Not set',
+        'Other teams': 'Not set',
+      },
     })
+
+    cy.contains('a', 'Edit event')
   })
 
-  describe('Disabled event with no document', () => {
-    beforeEach(() => {
-      cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id))
+  it('should display no document link details', () => {
+    cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id))
+
+    assertBreadcrumbs({
+      Home: urls.dashboard.index.route,
+      Events: urls.events.index(),
+      'Teddy bear expo': null,
+      Details: null,
     })
 
-    it('should display no document link details', () => {
-      assertBreadcrumbs({
-        Home: urls.dashboard.index.route,
-        Events: urls.events.index(),
-        'Teddy bear expo': null,
-      })
-
-      assertKeyValueTable('eventDetails', {
-        'Type of event': 'Exhibition',
-        'Event date': '1 January 2021',
-        'Event location type': 'HQ',
-        Address:
-          'Day Court Exhibition Centre, Day Court Lane, China, SW9 9AB, China',
-        Region: 'Not set',
-        Notes: 'This is a dummy event for testing.',
-        'Lead team': 'CBBC Hangzhou',
-        Organiser: 'John Rogers',
-        'Other teams': 'CBBC HangzhouCBBC North West',
-        'Related programmes': 'Grown in Britain',
+    assertSummaryTableStrict({
+      rows: {
+        ...EVENT_DETAILS_ROWS,
         'Related Trade Agreements': 'Not set',
-        Service: 'Events : UK based',
-      })
+      },
     })
 
-    it('should hide edit event button for disabled events', () => {
-      cy.get(selectors.entityCollection.editEvent).should('not.exist')
-    })
+    cy.contains('a', 'Edit event').should('not.exist')
   })
 
-  describe('Certain fields that are null', () => {
-    before(() => {
-      cy.intercept(
-        'GET',
-        `/api-proxy/v4/event/${fixtures.event.teddyBearExpo.id}`,
-        {
-          address_1: '16 Grande Parade',
-          address_2: '',
-          address_country: {
-            name: 'China',
-            id: '63af72a6-5d95-e211-a939-e4115bead28a',
-          },
-          address_county: '',
-          address_postcode: '',
-          address_town: 'Shanghai',
-          end_date: '2016-03-16',
-          event_type: {
-            name: 'Exhibition',
-            id: '2fade471-e868-4ea9-b125-945eb90ae5d4',
-          },
-          lead_team: {
-            name: 'UK Fashion and Textile Association Ltd (UKFT)',
-            id: '23f12898-9698-e211-a939-e4115bead28a',
-          },
-          name: 'Shanghai Fashion Week including The Hub, Chic-Pure-Intertextile March 2016',
-          notes: null,
-          organiser: null,
-          start_date: '2016-03-16',
-          service: {
-            name: 'DBT export service or funding : UK Tradeshow Programme (UKTP) – exhibitor',
-            id: '380bba2b-3499-e211-a939-e4115bead28a',
-          },
-          uk_region: null,
-        }
-      ).as('apiRequest')
-      cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id))
-      cy.wait('@apiRequest')
-    })
+  it('should set fields that are null to "Not set"', () => {
+    cy.intercept(
+      'GET',
+      `/api-proxy/v4/event/${fixtures.event.teddyBearExpo.id}`,
+      {
+        address_1: '16 Grande Parade',
+        address_2: '',
+        address_country: {
+          name: 'China',
+          id: '63af72a6-5d95-e211-a939-e4115bead28a',
+        },
+        address_county: '',
+        address_postcode: '',
+        address_town: 'Shanghai',
+        end_date: '2016-03-16',
+        event_type: {
+          name: 'Exhibition',
+          id: '2fade471-e868-4ea9-b125-945eb90ae5d4',
+        },
+        lead_team: {
+          name: 'UK Fashion and Textile Association Ltd (UKFT)',
+          id: '23f12898-9698-e211-a939-e4115bead28a',
+        },
+        name: 'Shanghai Fashion Week including The Hub, Chic-Pure-Intertextile March 2016',
+        notes: null,
+        organiser: null,
+        start_date: '2016-03-16',
+        service: {
+          name: 'DBT export service or funding : UK Tradeshow Programme (UKTP) – exhibitor',
+          id: '380bba2b-3499-e211-a939-e4115bead28a',
+        },
+        uk_region: null,
+      }
+    )
+
+    cy.visit(urls.events.details(fixtures.event.teddyBearExpo.id))
 
-    it('should set fields that are null to "Not set"', () => {
-      assertKeyValueTable('eventDetails', {
-        'Type of event': 'Exhibition',
+    assertSummaryTableStrict({
+      rows: {
+        ...EVENT_DETAILS_ROWS,
         'Event date': '16 March 2016',
         'Event location type': 'Not set',
         Address: '16 Grande Parade, Shanghai, China',
@@ -148,7 +127,7 @@ describe('Event Details', () => {
         'Related Trade Agreements': 'Not set',
         Service:
           'DBT export service or funding : UK Tradeshow Programme (UKTP) – exhibitor',
-      })
+      },
     })
   })
 })
diff --git a/test/functional/cypress/support/assertions.js b/test/functional/cypress/support/assertions.js
index 6ea7c95dfb5..97057aec34d 100644
--- a/test/functional/cypress/support/assertions.js
+++ b/test/functional/cypress/support/assertions.js
@@ -76,13 +76,15 @@ const assertSummaryTable = ({
 }
 
 /**
- * @param {{rows: [string, string | number][], caption?: string}} options
+ * @param {{rows: [string, string | number][] | Record<string, string>, caption?: string}} options
  */
 const assertSummaryTableStrict = ({ caption, rows }) => {
+  const _rows = Array.isArray(rows) ? rows : Object.entries(rows)
+
   const assertRows = (el) => {
-    cy.wrap(el).find('tr').as('rows').should('have.length', rows.length)
+    cy.wrap(el).find('tr').as('rows').should('have.length', _rows.length)
 
-    rows.forEach(([key, val], i) => {
+    _rows.forEach(([key, val], i) => {
       cy.get('@rows')
         .eq(i)
         .within(() => {
diff --git a/test/functional/cypress/support/utils.js b/test/functional/cypress/support/utils.js
deleted file mode 100644
index be6451a1257..00000000000
--- a/test/functional/cypress/support/utils.js
+++ /dev/null
@@ -1,5 +0,0 @@
-module.exports = {
-  randomString: () => {
-    return Math.random().toString(36).substring(7)
-  },
-}
diff --git a/test/selectors/entity-collection.js b/test/selectors/entity-collection.js
index b046069ac36..98f03103776 100644
--- a/test/selectors/entity-collection.js
+++ b/test/selectors/entity-collection.js
@@ -1,6 +1,4 @@
 module.exports = {
-  addAttendee: '[data-test="Add attendee"]',
-  editEvent: 'a:contains("Edit event")',
   addProposition: '[data-test="add-collection-item-button"]',
   collection: '.c-collection',
   sortBy: '[name="sortBy"] > select',