diff --git a/src/Pages/Profile.vue b/src/Pages/Profile.vue
new file mode 100644
index 0000000000000000000000000000000000000000..325c341a1ec36fc0edfb0bdd8064541f55276933
--- /dev/null
+++ b/src/Pages/Profile.vue
@@ -0,0 +1,16 @@
+<template>
+  <router-view v-if="host" :profile="host" />
+</template>
+
+<script setup lang="ts">
+import { useRoute } from 'vue-router'
+import { useObjectFromStore } from '@rokoli/bnb/drf'
+import { useHostStore } from '@/stores'
+
+const route = useRoute()
+const hostStore = useHostStore()
+const { obj: host } = useObjectFromStore(
+  () => parseInt(route.params.profileId as string),
+  hostStore,
+)
+</script>
diff --git a/src/Pages/ProfileDetails.vue b/src/Pages/ProfileDetails.vue
new file mode 100644
index 0000000000000000000000000000000000000000..1892721ecb47ac8fdf86a5708c4f7baf619a8459
--- /dev/null
+++ b/src/Pages/ProfileDetails.vue
@@ -0,0 +1,73 @@
+<template>
+  <PageHeader :title="profile.name" :editing-metadata="profile" />
+
+  <div class="tw-flex tw-gap-6 tw-items-start tw-flex-wrap">
+    <ASection :title="t('profile.singular')" class="tw-w-fit tw-flex-none">
+      <template #header>
+        <AProfileDeletionFlow :profile="profile" standalone class="tw-ml-auto" />
+      </template>
+
+      <AFieldset
+        class="tw-bg-white md:tw-min-w-[500px] tw-max-w-2xl"
+        :title="t('profileDetails.data')"
+      >
+        <AProfileEditor :profile="profile" admin-mode />
+      </AFieldset>
+    </ASection>
+
+    <ASection
+      v-if="owners.value.length > 0"
+      :title="t('profileDetails.assignedAccounts')"
+      class="tw-flex-1 md:tw-flex-none tw-w-fit"
+    >
+      <div class="tw-flex tw-gap-6 tw-items-start tw-flex-wrap">
+        <template v-for="user in owners.value" :key="user.id">
+          <AUserIdentity v-slot="{ identity }" :user="user">
+            <AFieldset
+              as="form"
+              title-tag="h3"
+              :title="identity.name || identity.username"
+              class="tw-bg-white md:tw-min-w-[380px] tw-max-w-lg tw-flex-1"
+            >
+              <AUserEditor :user="user" />
+            </AFieldset>
+          </AUserIdentity>
+        </template>
+      </div>
+    </ASection>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { useRelationList } from '@/form'
+import { useI18n } from '@/i18n'
+import { useHostStore, useUserStore } from '@/stores'
+import { useBreadcrumbs } from '@/stores/nav'
+import { Host } from '@/types'
+
+import PageHeader from '@/components/PageHeader.vue'
+import AUserIdentity from '@/components/identities/AUserIdentity.vue'
+import AUserEditor from '@/components/identities/AUserEditor.vue'
+import AProfileEditor from '@/components/identities/AProfileEditor.vue'
+import AFieldset from '@/components/generic/AFieldset.vue'
+import ASection from '@/components/generic/ASection.vue'
+import AProfileDeletionFlow from '@/components/identities/AProfileDeletionFlow.vue'
+
+const props = defineProps<{
+  profile: Host
+}>()
+
+const { t } = useI18n()
+const hostStore = useHostStore()
+const userStore = useUserStore()
+
+const owners = useRelationList(hostStore, () => props.profile, 'ownerIds', userStore)
+
+useBreadcrumbs(() => [
+  { title: t('navigation.profiles'), route: { name: 'profiles' } },
+  {
+    title: props.profile.name,
+    route: { name: 'profile', params: { profileId: props.profile.id.toString() } },
+  },
+])
+</script>
diff --git a/src/Pages/ProfileList.vue b/src/Pages/ProfileList.vue
new file mode 100644
index 0000000000000000000000000000000000000000..d380baaf5e8816aaeef6252d0b84cf42087751a0
--- /dev/null
+++ b/src/Pages/ProfileList.vue
@@ -0,0 +1,80 @@
+<template>
+  <PageHeader :title="t('profileList.title')" />
+
+  <ATable
+    v-model:page="page"
+    v-model:items-per-page="profilesPerPage"
+    :items="result.items"
+    :pagination-data="result"
+  >
+    <template #tableStart>
+      <colgroup>
+        <col width="0" />
+        <col width="0" />
+      </colgroup>
+    </template>
+    <template #header>
+      <th>{{ t('profile.fields.name') }}</th>
+    </template>
+    <template #items="{ items }">
+      <tr v-for="profile in items" :key="profile.id" class="tw-transition hover:tw-bg-gray-50">
+        <td>
+          <RouterLink
+            :to="{ name: 'profile-details', params: { profileId: profile.id } }"
+            class="tw-text-inherit tw-group tw-no-underline"
+          >
+            <div class="tw-flex tw-gap-3 tw-items-center">
+              <Image :image="profile.imageId" class="tw-w-12 tw-bg-gray-300 tw-rounded" square />
+              <span class="tw-flex tw-flex-col tw-gap-1">
+                <span class="tw-block group-hocus:tw-underline">{{ profile.name }}</span>
+                <span class="tw-flex tw-gap-2 tw-items-center tw-text-xs tw-leading-loose">
+                  <AColorBadge
+                    class="tw-rounded-full tw-px-2"
+                    :class="profile.isActive ? 'tw-text-emerald-700' : 'tw-text-rose-700'"
+                  >
+                    {{ profile.isActive ? t('active') : t('inactive') }}
+                  </AColorBadge>
+
+                  <AColorBadge
+                    v-if="profile.ownerIds.length > 0"
+                    class="tw-rounded-full tw-px-2 tw-text-blue-700 tw-inline-flex tw-gap-1 tw-items-center"
+                  >
+                    <icon-fluent-key-20-regular class="tw-flex-none" />
+                    {{ t('profile.labels.hasAccount') }}
+                  </AColorBadge>
+                </span>
+              </span>
+            </div>
+          </RouterLink>
+        </td>
+      </tr>
+    </template>
+  </ATable>
+</template>
+<script setup lang="ts">
+import ATable from '@/components/generic/ATable.vue'
+
+import { computedDebounced, useQuery } from '@/util'
+import { useStorage } from '@vueuse/core'
+import { ref } from 'vue'
+import { usePaginatedList } from '@rokoli/bnb/drf'
+import { useHostStore } from '@/stores'
+import PageHeader from '@/components/PageHeader.vue'
+import { useI18n } from '@/i18n'
+import AColorBadge from '@/components/generic/AColorBadge.vue'
+import Image from '@/components/generic/Image.vue'
+
+const { t } = useI18n()
+const hostStore = useHostStore()
+
+const searchTerm = ref('')
+const debouncedSearchTerm = computedDebounced(searchTerm, (q: string) => (q.trim() ? 0.3 : 0))
+const query = useQuery(() => ({
+  search: debouncedSearchTerm.value.trim(),
+}))
+const page = ref(1)
+const profilesPerPage = useStorage('aura:profileList:profilesPerPage', 12)
+const { result } = usePaginatedList(hostStore.listIsolated, page, profilesPerPage, {
+  query,
+})
+</script>
diff --git a/src/components/identities/AProfileDeletionFlow.vue b/src/components/identities/AProfileDeletionFlow.vue
new file mode 100644
index 0000000000000000000000000000000000000000..febbd7707543741c60733c88be9fb0d1cc4d8f50
--- /dev/null
+++ b/src/components/identities/AProfileDeletionFlow.vue
@@ -0,0 +1,83 @@
+<template>
+  <FormGroup :errors="errors" v-bind="attrs">
+    <div>
+      <button
+        type="button"
+        class="btn tw-w-full tw-justify-center"
+        :class="standalone ? 'btn-danger' : 'btn-default'"
+        @click="confirmDeleteProfile"
+      >
+        <Loading v-if="isProcessing" class="tw-h-2" />
+        {{ t('profile.editor.deletion.label') }}
+      </button>
+      <ADescription v-if="!standalone">{{ t('profile.editor.deletion.description') }}</ADescription>
+    </div>
+  </FormGroup>
+
+  <ConfirmDeletion v-slot="{ resolve }">
+    <Teleport to="body">
+      <AConfirmDialog
+        :title="t('profile.editor.deletion.label')"
+        :confirm-label="
+          t('profile.editor.deletion.confirm.confirmLabel', { profile: profile.name })
+        "
+        class="tw-max-w-xl"
+        @confirm="resolve(true)"
+        @cancel="resolve(false)"
+      >
+        <SafeHTML
+          as="p"
+          :html="t('profile.editor.deletion.confirm.text', { profile: profile.name })"
+          sanitize-preset="inline-noninteractive"
+        />
+      </AConfirmDialog>
+    </Teleport>
+  </ConfirmDeletion>
+</template>
+<script setup lang="ts">
+import { createTemplatePromise } from '@vueuse/core'
+import { useRouter } from 'vue-router'
+import { useErrorList } from '@rokoli/bnb/drf'
+import { useAttrs } from 'vue'
+
+import { useAsyncFunction } from '@/util'
+import { Host } from '@/types'
+import { useI18n } from '@/i18n'
+import { useHostStore } from '@/stores'
+
+import FormGroup from '@/components/generic/FormGroup.vue'
+import Loading from '@/components/generic/Loading.vue'
+import SafeHTML from '@/components/generic/SafeHTML'
+import AConfirmDialog from '@/components/generic/AConfirmDialog.vue'
+import ADescription from '@/components/generic/ADescription.vue'
+
+const props = defineProps<{
+  profile: Host
+  standalone?: boolean
+}>()
+
+const { t } = useI18n()
+const router = useRouter()
+const hostStore = useHostStore()
+const attrs = useAttrs()
+
+const ConfirmDeletion = createTemplatePromise<boolean>()
+
+const {
+  fn: deleteProfile,
+  isProcessing,
+  error,
+} = useAsyncFunction(() => hostStore.remove(props.profile.id))
+const errors = useErrorList(error)
+
+async function confirmDeleteProfile() {
+  if (await ConfirmDeletion.start()) {
+    try {
+      await deleteProfile()
+      await router.push({ name: 'profiles' })
+    } catch (e) {
+      // error handling is done above
+    }
+  }
+}
+</script>
diff --git a/src/components/identities/AProfileEditor.vue b/src/components/identities/AProfileEditor.vue
new file mode 100644
index 0000000000000000000000000000000000000000..409002ed11bc4b04e4f0db0ee6f5aa48dc6fbe2e
--- /dev/null
+++ b/src/components/identities/AProfileEditor.vue
@@ -0,0 +1,138 @@
+<!--
+This is an editor for the Host steering model.
+-->
+
+<template>
+  <FormTable>
+    <FormGroup
+      v-slot="attrs"
+      :label="t('profile.fields.name')"
+      :errors="name.errors"
+      :is-saving="name.isSaving"
+    >
+      <input v-model="name.value" required v-bind="attrs" @blur="name.save()" />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="attrs"
+      :label="t('profile.fields.email')"
+      :errors="email.errors"
+      :is-saving="email.isSaving"
+    >
+      <input v-model="email.value" type="email" required v-bind="attrs" @blur="email.save()" />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="{ id, disabled }"
+      :label="t('profile.fields.biography')"
+      :errors="biography.errors"
+      :is-saving="biography.isSaving"
+      custom-control
+    >
+      <AHTMLEditor
+        :id="id"
+        v-model="biography.value"
+        :disabled="disabled"
+        @blur="biography.save()"
+      />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="{ disabled }"
+      :label="t('profile.fields.image')"
+      :errors="imageId.errors"
+      :is-saving="imageId.isSaving"
+      custom-control
+    >
+      <ImagePicker v-model="imageId.value" :disabled="disabled" class="tw-flex-none tw-w-min" />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="{ disabled }"
+      :label="t('profile.fields.links')"
+      :is-saving="links.isSaving"
+      :errors="links.errors.forField('links', '')"
+      :has-error="links.errors.length > 0"
+      custom-control
+    >
+      <ALinkCollectionEditor
+        v-model="links.value"
+        :error-lists="links.errors.siblings('links')"
+        :disabled="disabled"
+        allow-add
+        @save="links.save()"
+      />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="attrs"
+      :label="t('profile.fields.isActive')"
+      :errors="isActive.errors"
+      :is-saving="isActive.isSaving"
+      custom-control
+    >
+      <label class="tw-inline-flex tw-gap-2 tw-items-center">
+        <input
+          v-model="isActive.value"
+          type="checkbox"
+          class="tw-size-5"
+          :true-value="true"
+          :false-value="false"
+          v-bind="attrs"
+          switch
+          @blur="isActive.save()"
+        />
+        {{ t('profile.fields.isActive') }}
+      </label>
+    </FormGroup>
+
+    <template v-if="adminMode">
+      <FormGroup
+        v-slot="{ disabled }"
+        :label="t('profile.fields.ownerIds')"
+        class="tw-order-last"
+        :is-saving="owners.isSaving"
+        :errors="owners.errors"
+      >
+        <AUserSelector v-model="owners.value" :disabled="disabled" />
+      </FormGroup>
+    </template>
+  </FormTable>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+
+import { useAPIObjectFieldCopy, useRelationList } from '@/form'
+import { useI18n } from '@/i18n'
+import { useHostStore, useUserStore } from '@/stores'
+import { Host } from '@/types'
+
+import FormTable from '@/components/generic/FormTable.vue'
+import FormGroup from '@/components/generic/FormGroup.vue'
+import AHTMLEditor from '@/components/generic/AHTMLEditor.vue'
+import ImagePicker from '@/components/images/ImagePicker.vue'
+import ALinkCollectionEditor from '@/components/generic/ALinkCollectionEditor.vue'
+import AUserSelector from '@/components/identities/AUserSelector.vue'
+
+const props = defineProps<{
+  profile: Host
+  adminMode?: boolean
+}>()
+
+const { t } = useI18n()
+const hostStore = useHostStore()
+const userStore = useUserStore()
+const profile = computed(() => props.profile)
+
+const name = useAPIObjectFieldCopy(hostStore, profile, 'name', { debounce: 2 })
+const biography = useAPIObjectFieldCopy(hostStore, profile, 'biography', { noAutoSave: true })
+const email = useAPIObjectFieldCopy(hostStore, profile, 'email', { debounce: 2 })
+const imageId = useAPIObjectFieldCopy(hostStore, profile, 'imageId', { debounce: 0 })
+const links = useAPIObjectFieldCopy(hostStore, profile, 'links', { debounce: 2 })
+const isActive = useAPIObjectFieldCopy(hostStore, profile, 'isActive', { debounce: 0 })
+const owners = useRelationList(hostStore, profile, 'ownerIds', userStore, {
+  debounce: 2,
+  sortBy: ['lastName', 'firstName', 'username', 'email'],
+})
+</script>
diff --git a/src/components/identities/AUserCBAIdentityEditor.vue b/src/components/identities/AUserCBAIdentityEditor.vue
new file mode 100644
index 0000000000000000000000000000000000000000..15563ddb17f64b894c5fa145f62e00a90fa8ca8b
--- /dev/null
+++ b/src/components/identities/AUserCBAIdentityEditor.vue
@@ -0,0 +1,43 @@
+<template>
+  <fieldset class="tw-flex tw-flex-col tw-gap-2" @focusout="save()">
+    <FormGroup v-slot="attrs" :label="t('user.fields.profile.cbaUsername')">
+      <input v-model="name" v-bind="attrs" />
+    </FormGroup>
+
+    <FormGroup v-slot="attrs" :label="t('user.fields.profile.cbaUserToken')">
+      <input v-model="token" type="password" v-bind="attrs" />
+    </FormGroup>
+  </fieldset>
+</template>
+
+<script lang="ts" setup>
+import { SteeringUser } from '@/stores/auth'
+import { useCopy } from '@/form'
+import FormGroup from '@/components/generic/FormGroup.vue'
+import { useI18n } from '@/i18n'
+
+type CBAProfileData = Pick<SteeringUser['profile'], 'cbaUsername' | 'cbaUserToken'>
+
+const modelValue = defineModel<CBAProfileData>({ required: true })
+const { t } = useI18n()
+
+const name = useCopy(() => modelValue.value?.cbaUsername ?? '', {
+  debounce: 0,
+  save: (cbaUsername) => {
+    save({ cbaUsername })
+  },
+})
+const token = useCopy(() => modelValue.value?.cbaUserToken ?? '', {
+  debounce: 0,
+  save: (cbaUserToken) => {
+    save({ cbaUserToken })
+  },
+})
+
+function save(data?: Partial<CBAProfileData>) {
+  let { cbaUsername, cbaUserToken } = data ?? {}
+  cbaUsername = (cbaUsername ?? name.value)?.trim()
+  cbaUserToken = (cbaUserToken ?? token.value)?.trim()
+  if (cbaUsername && cbaUserToken) modelValue.value = { cbaUsername, cbaUserToken }
+}
+</script>
diff --git a/src/components/identities/AUserEditor.vue b/src/components/identities/AUserEditor.vue
new file mode 100644
index 0000000000000000000000000000000000000000..dce82b2f7143dd26029380c7954701bdaff5a748
--- /dev/null
+++ b/src/components/identities/AUserEditor.vue
@@ -0,0 +1,73 @@
+<template>
+  <FormTable>
+    <FormGroup v-slot="attrs" :label="t('user.fields.username')">
+      <input v-model="user.username" required v-bind="attrs" disabled />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="attrs"
+      :label="t('user.fields.firstName')"
+      :errors="firstName.errors"
+      :is-saving="firstName.isSaving"
+    >
+      <input v-model="firstName.value" required v-bind="attrs" @blur="firstName.save()" />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="attrs"
+      :label="t('user.fields.lastName')"
+      :errors="lastName.errors"
+      :is-saving="lastName.isSaving"
+    >
+      <input v-model="lastName.value" required v-bind="attrs" @blur="lastName.save()" />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="attrs"
+      :label="t('user.fields.email')"
+      :errors="email.errors"
+      :is-saving="email.isSaving"
+    >
+      <input v-model="email.value" type="email" required v-bind="attrs" @blur="email.save()" />
+    </FormGroup>
+
+    <FormGroup
+      v-slot="{ disabled }"
+      :errors="profile.errors"
+      :is-saving="profile.isSaving"
+      label="CBA"
+      custom-control
+    >
+      <AUserCBAIdentityEditor v-model="profile.value" :disabled="disabled" />
+    </FormGroup>
+  </FormTable>
+</template>
+
+<script lang="ts" setup>
+import { computed } from 'vue'
+import { useAPIObjectFieldCopy } from '@/form'
+import { useI18n } from '@/i18n'
+import { SteeringUser, useUserStore } from '@/stores/auth'
+
+import FormTable from '@/components/generic/FormTable.vue'
+import FormGroup from '@/components/generic/FormGroup.vue'
+import AUserCBAIdentityEditor from '@/components/identities/AUserCBAIdentityEditor.vue'
+
+const props = defineProps<{
+  user: SteeringUser
+}>()
+
+const { t } = useI18n()
+const userStore = useUserStore()
+const user = computed(() => props.user)
+
+const firstName = useAPIObjectFieldCopy(userStore, user, 'firstName', { debounce: 2 })
+const lastName = useAPIObjectFieldCopy(userStore, user, 'lastName', { debounce: 2 })
+const email = useAPIObjectFieldCopy(userStore, user, 'email', { debounce: 2 })
+const profile = useAPIObjectFieldCopy(userStore, user, 'profile', {
+  debounce: 0,
+  isEqual(v1, v2) {
+    return v1?.cbaUsername === v2?.cbaUsername && v1?.cbaUserToken === v2?.cbaUserToken
+  },
+})
+</script>
diff --git a/src/components/identities/AUserIdentity.vue b/src/components/identities/AUserIdentity.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4b38cfda0ea495a34edbfae1688f188f49db574d
--- /dev/null
+++ b/src/components/identities/AUserIdentity.vue
@@ -0,0 +1,13 @@
+<template>
+  <slot :identity="identity" />
+</template>
+
+<script lang="ts" setup>
+import { SteeringUser } from '@/stores/auth'
+import { usePersonName } from '@/util'
+
+const props = defineProps<{
+  user: SteeringUser
+}>()
+const identity = usePersonName(() => props.user)
+</script>
diff --git a/src/components/nav/AMainNavMenu.vue b/src/components/nav/AMainNavMenu.vue
index 51c2dbf833de72f3f4d4264ae8faa68683a3b2ea..f824afffcae2edc0a992c266efe8c89707717921 100644
--- a/src/components/nav/AMainNavMenu.vue
+++ b/src/components/nav/AMainNavMenu.vue
@@ -46,6 +46,12 @@
         </ANavSubMenu>
       </ANavListItem>
 
+      <ANavListItem>
+        <ANavLink :route="{ name: 'profiles' }" active-if-child-active>
+          {{ t('navigation.profiles') }}
+        </ANavLink>
+      </ANavListItem>
+
       <ANavListItem>
         <ANavLink :route="{ name: 'calendar' }">
           {{ t('navigation.calendar') }}
diff --git a/src/i18n/de.js b/src/i18n/de.js
index bd96153747f15cb555c1600ee0968fef73454901..44aec4c8d1661a0dc710f11c558cf5c903551806 100644
--- a/src/i18n/de.js
+++ b/src/i18n/de.js
@@ -29,6 +29,34 @@ export default {
     otherShows: 'Weitere Sendereihen',
   },
 
+  profile: {
+    singular: 'Profil',
+    plural: 'Profiles',
+    fields: {
+      name: 'Name',
+      email: 'Email',
+      biography: 'Biographie',
+      image: 'Bild',
+      links: 'Links',
+      isActive: 'Aktiv',
+      ownerIds: 'Zugewiesene Nutzer:innenkonten',
+    },
+    labels: {
+      hasAccount: 'Hat Konto',
+    },
+    editor: {
+      deletion: {
+        label: 'Profil löschen',
+        description:
+          'Entfernt das Profil und alle bestehenden Verknüpfungen zu Sendereihen und Episoden.',
+        confirm: {
+          text: 'Das Profil wird <strong>unwiederbringlich gelöscht</strong>. Eine spätere Wiederherstellung ist nicht möglich. Bestehende Verknüpfungen zu Sendereihen und Episoden werden ebenfalls entfernt. Bist du sicher, dass du das Profil <strong>%{profile}</strong> löschen willst?',
+          confirmLabel: 'Lösche <strong>%{profile}</strong>',
+        },
+      },
+    },
+  },
+
   show: {
     singular: 'Sendereihe',
     plural: 'Sendereihen',
@@ -164,6 +192,10 @@ export default {
   loadingData: 'Lade %{items}',
   loading: 'Lädt..',
   cancel: 'Abbrechen',
+  yes: 'Ja',
+  no: 'Nein',
+  active: 'Aktiv',
+  inactive: 'Inaktiv',
   help: 'Hilfe',
   delete: 'Löschen',
   add: 'Hinzufügen',
@@ -205,6 +237,7 @@ export default {
       details: 'Beschreibung',
     },
     filesPlaylists: 'Medien',
+    profiles: 'Profile',
     calendar: 'Kalender',
     settings: 'Einstellungen',
     profile: 'Profil',
@@ -212,6 +245,15 @@ export default {
     help: 'Hilfe',
   },
 
+  profileList: {
+    title: 'Profile',
+  },
+
+  profileDetails: {
+    assignedAccounts: 'Zugewiesene Konten',
+    data: 'Daten',
+  },
+
   footer: {
     tagline: 'Alles was Du für ein Freies Radio brauchst',
   },
@@ -422,8 +464,14 @@ export default {
   user: {
     fields: {
       username: 'Benutzername',
+      firstName: 'Vorname',
+      lastName: 'Nachname',
       name: 'Name',
       email: 'E-Mail',
+      profile: {
+        cbaUsername: 'Kontoname',
+        cbaUserToken: 'Token',
+      },
     },
   },
 
diff --git a/src/i18n/en.js b/src/i18n/en.js
index 47342cdaabaa9af8b8800a626d3aded2920a5232..ff507ee7bdfd0ef193fbc47bb12dde4a6dca5b0c 100644
--- a/src/i18n/en.js
+++ b/src/i18n/en.js
@@ -29,6 +29,33 @@ export default {
     otherShows: 'Other Shows',
   },
 
+  profile: {
+    singular: 'Profile',
+    plural: 'Profiles',
+    fields: {
+      name: 'Name',
+      email: 'Email',
+      biography: 'Biography',
+      image: 'Image',
+      links: 'Links',
+      isActive: 'Active',
+      ownerIds: 'Assigned user accounts',
+    },
+    labels: {
+      hasAccount: 'Has account',
+    },
+    editor: {
+      deletion: {
+        label: 'Delete profile',
+        description: 'Removes the profile and all existing relations to shows and episodes.',
+        confirm: {
+          text: 'The profile will be <strong>irretrievably deleted</strong>. It is not possible to restore it later. Existing relations to shows and episodes will be deleted as well. Are you sure you want to delete the profile <strong>%{profile}</strong>?',
+          confirmLabel: 'Delete <strong>%{profile}</strong>',
+        },
+      },
+    },
+  },
+
   show: {
     singular: 'Show',
     plural: 'Shows',
@@ -163,6 +190,10 @@ export default {
   loadingData: 'Loading %{items}',
   loading: 'Loading..',
   cancel: 'Cancel',
+  yes: 'Yes',
+  no: 'No',
+  active: 'Active',
+  inactive: 'Inactive',
   help: 'Help',
   delete: 'Delete',
   new: 'New',
@@ -206,6 +237,7 @@ export default {
       details: 'Description',
     },
     filesPlaylists: 'Media',
+    profiles: 'Profiles',
     calendar: 'Calendar',
     settings: 'Settings',
     profile: 'Profile',
@@ -213,6 +245,15 @@ export default {
     help: 'Help',
   },
 
+  profileList: {
+    title: 'Profiles',
+  },
+
+  profileDetails: {
+    assignedAccounts: 'Assigned Accounts',
+    data: 'Data',
+  },
+
   footer: {
     tagline: 'All the UI you need to run a community radio',
   },
@@ -423,8 +464,14 @@ export default {
   user: {
     fields: {
       username: 'Username',
+      firstName: 'Forename',
+      lastName: 'Surname',
       name: 'Name',
       email: 'Email',
+      profile: {
+        cbaUsername: 'Username',
+        cbaUserToken: 'Token',
+      },
     },
   },
 
diff --git a/src/router.ts b/src/router.ts
index 8741a7cc90308570b126f3de8d3b34598c0176c6..f81dd4eb98c2f7c362123d64800ef21be256f900 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -45,6 +45,29 @@ const routes: RouteRecordRaw[] = [
       },
     ],
   },
+  {
+    path: '/profiles',
+    children: [
+      {
+        path: '/profiles',
+        name: 'profiles',
+        component: () => import('@/Pages/ProfileList.vue'),
+      },
+      {
+        path: ':profileId',
+        name: 'profile',
+        component: () => import('@/Pages/Profile.vue'),
+        redirect: (to) => ({ path: `/profiles/${to.params.profileId}/details` }),
+        children: [
+          {
+            path: 'details',
+            name: 'profile-details',
+            component: () => import('@/Pages/ProfileDetails.vue'),
+          },
+        ],
+      },
+    ],
+  },
   { path: '/calendar', name: 'calendar', component: () => import('@/Pages/Calendar.vue') },
   { path: '/credits', name: 'credits', component: () => import('@/Pages/Credits.vue') },
 ]
diff --git a/src/steering-types.ts b/src/steering-types.ts
index 5a68be04bab6766cd5c69f6933ebfd1e75551354..0ae4a268c3c61159e520652a15acd8e16693f6cb 100644
--- a/src/steering-types.ts
+++ b/src/steering-types.ts
@@ -468,7 +468,7 @@ export interface components {
       isActive?: boolean
       links?: components['schemas']['HostLink'][]
       name: string
-      ownerIds: (number | null)[]
+      ownerIds: number[]
       /** Format: date-time */
       createdAt: string
       createdBy: string