return $guild->toJson();
        }
 
+       public function manageSubscriptions(Request $request, $guild_id) {
+               $guild = DiscordGuild::where('guild_id', '=', $guild_id)->firstOrFail();
+               $this->authorize('manage', $guild);
+
+               $validatedData = $request->validate([
+                       'add_event' => 'numeric|exists:App\Models\Event,id',
+                       'add_user' => 'numeric|exists:App\Models\User,id',
+                       'remove_event' => 'numeric|exists:App\Models\Event,id',
+                       'remove_user' => 'numeric|exists:App\Models\User,id',
+               ]);
+
+               if (isset($validatedData['add_event'])) {
+                       $guild->event_subscriptions()->create(['event_id' => $validatedData['add_event']]);
+               }
+               if (isset($validatedData['add_user'])) {
+                       $guild->user_subscriptions()->create(['user_id' => $validatedData['add_user']]);
+               }
+               if (isset($validatedData['remove_event'])) {
+                       $guild->event_subscriptions()->where('event_id', '=', $validatedData['remove_event'])->delete();
+               }
+               if (isset($validatedData['remove_user'])) {
+                       $guild->user_subscriptions()->where('user_id', '=', $validatedData['remove_user'])->delete();
+               }
+               $guild->load([
+                       'event_subscriptions',
+                       'event_subscriptions.event',
+                       'user_subscriptions',
+                       'user_subscriptions.user',
+               ]);
+
+               return $guild->toJson();
+       }
+
 }
 
                $validatedData = $request->validate([
                        'after' => 'nullable|date',
                        'before' => 'nullable|date',
+                       'exclude_ids' => 'array|nullable',
+                       'exclude_ids.*' => 'int',
+                       'limit' => 'nullable|int',
                        'order' => 'nullable|string',
+                       'phrase' => 'nullable|string',
                        'with' => 'nullable|array',
                        'with.*' => 'string',
                ]);
                $events = Event::where('visible', '=', true);
+               if (!empty($validatedData['exclude_ids'])) {
+                       $events->whereNotIn('id', $validatedData['exclude_ids']);
+               }
                if (isset($validatedData['before'])) {
                        $events = $events->where(function ($query) use ($validatedData) {
                                $query->whereNull('start');
                                $query->orWhere('end', '>', $validatedData['after']);
                        });
                }
+               if (isset($validatedData['limit'])) {
+                       $events->limit($validatedData['limit']);
+               }
                if (isset($validatedData['order'])) {
                        switch ($validatedData['order']) {
                                case 'recency':
                                        break;
                        }
                }
+               if (isset($validatedData['phrase'])) {
+                       $events->where('title', 'LIKE', '%'.$validatedData['phrase'].'%');
+               }
                if (isset($validatedData['with'])) {
                        if (in_array('description', $validatedData['with'])) {
                                $events->with('description');
 
 
        public function search(Request $request) {
                $validatedData = $request->validate([
+                       'exclude_ids' => 'array|nullable',
+                       'exclude_ids.*' => 'string',
                        'phrase' => 'string|nullable',
                ]);
 
                $users = User::query();
+               if (!empty($validatedData['exclude_ids'])) {
+                       $users->whereNotIn('id', $validatedData['exclude_ids']);
+               }
                if (!empty($validatedData['phrase'])) {
-                       $users = $users->where('username', 'LIKE', '%'.$validatedData['phrase'].'%')
+                       $users->where('username', 'LIKE', '%'.$validatedData['phrase'].'%')
                                ->orWhere('nickname', 'LIKE', '%'.$validatedData['phrase'].'%');
                }
                $users = $users->limit(5);
 
        public function setLanguage(Request $request) {
                $user = $request->user();
-               if (!$user) return;
+               if (!$user) {
+                       return;
+               }
 
                $validatedData = $request->validate([
                        'language' => 'required|in:de,en',
 
                return $this->belongsTo(DiscordGuild::class);
        }
 
+       protected $fillable = [
+               'discord_guild_id',
+               'event_id',
+       ];
+
 }
 
                return $this->belongsTo(User::class);
        }
 
+       protected $fillable = [
+               'discord_guild_id',
+               'user_id',
+       ];
+
+       protected $casts = [
+               'user_id' => 'string',
+       ];
+
 }
 
--- /dev/null
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+       /**
+        * Run the migrations.
+        */
+       public function up(): void {
+               Schema::table('discord_guild_event_subscriptions', function (Blueprint $table) {
+                       $table->unique(['discord_guild_id', 'event_id'], 'guild_event_unique');
+               });
+               Schema::table('discord_guild_user_subscriptions', function (Blueprint $table) {
+                       $table->unique(['discord_guild_id', 'user_id'], 'guild_user_unique');
+               });
+       }
+
+       /**
+        * Reverse the migrations.
+        */
+       public function down(): void {
+               Schema::table('discord_guild_event_subscriptions', function (Blueprint $table) {
+                       $table->dropIndex('guild_event_unique');
+               });
+               Schema::table('discord_guild_user_subscriptions', function (Blueprint $table) {
+                       $table->dropIndex('guild_user_unique');
+               });
+       }
+};
 
                        </Button>
                </div>;
        }
-       return <div className={`discord-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+       return <div className={`model-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
                <Form.Control
                        className="search-input"
                        name={Math.random().toString(20).substr(2, 10)}
 
                        </Button>
                </div>;
        }
-       return <div className={`discord-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+       return <div className={`model-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
                <Form.Control
                        className="search-input"
                        name={Math.random().toString(20).substr(2, 10)}
 
--- /dev/null
+import axios from 'axios';
+import PropTypes from 'prop-types';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { Button, Form, ListGroup } from 'react-bootstrap';
+
+import Icon from './Icon';
+import EventBox from '../events/Box';
+import debounce from '../../helpers/debounce';
+
+const EventSelect = ({ excludeIds = [], name, onChange, value }) => {
+       const [resolved, setResolved] = useState(null);
+       const [results, setResults] = useState([]);
+       const [search, setSearch] = useState('');
+       const [showResults, setShowResults] = useState(false);
+
+       const ref = useRef(null);
+
+       useEffect(() => {
+               const handleEventOutside = e => {
+                       if (ref.current && !ref.current.contains(e.target)) {
+                               setShowResults(false);
+                       }
+               };
+               document.addEventListener('mousedown', handleEventOutside, true);
+               document.addEventListener('focus', handleEventOutside, true);
+               return () => {
+                       document.removeEventListener('mousedown', handleEventOutside, true);
+                       document.removeEventListener('focus', handleEventOutside, true);
+               };
+       }, []);
+
+       let ctrl = null;
+       const fetch = useCallback(debounce(async phrase => {
+               if (ctrl) {
+                       ctrl.abort();
+               }
+               ctrl = new AbortController();
+               try {
+                       const response = await axios.get(`/api/events`, {
+                               params: {
+                                       exclude_ids: excludeIds,
+                                       limit: 5,
+                                       phrase,
+                               },
+                               signal: ctrl.signal,
+                       });
+                       ctrl = null;
+                       setResults(response.data);
+                       if (phrase) {
+                               setShowResults(true);
+                       }
+               } catch (e) {
+                       ctrl = null;
+                       console.error(e);
+               }
+       }, 300), [excludeIds]);
+
+       useEffect(() => {
+               fetch(search);
+       }, [search]);
+
+       useEffect(() => {
+               if (value) {
+                       axios
+                               .get(`/api/events/${value}`)
+                       .then(response => {
+                               setResolved(response.data);
+                       });
+               } else {
+                       setResolved(null);
+               }
+       }, [value]);
+
+       if (value) {
+               return <div className="d-flex justify-content-between">
+                       {resolved ? <EventBox event={resolved} /> : <span>value</span>}
+                       <Button
+                               onClick={() => onChange({ target: { name, value: null }})}
+                               size="sm"
+                               variant="outline-danger"
+                       >
+                               <Icon.REMOVE />
+                       </Button>
+               </div>;
+       }
+       return <div className={`model-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+               <Form.Control
+                       className="search-input"
+                       name={Math.random().toString(20).substr(2, 10)}
+                       onChange={e => setSearch(e.target.value)}
+                       onFocus={() => setShowResults(true)}
+                       type="search"
+                       value={search}
+               />
+               <div className="search-results-holder">
+                       <ListGroup className="search-results">
+                               {results.map(result =>
+                                       <ListGroup.Item
+                                               action
+                                               key={result.id}
+                                               onClick={() => {
+                                                       onChange({
+                                                               target: { name, value: result.id },
+                                                       });
+                                                       setSearch('');
+                                                       setShowResults(false);
+                                               }}
+                                       >
+                                               <EventBox event={result} />
+                                       </ListGroup.Item>
+                               )}
+                       </ListGroup>
+               </div>
+       </div>;
+};
+
+EventSelect.propTypes = {
+       excludeIds: PropTypes.arrayOf(PropTypes.number),
+       name: PropTypes.string,
+       onChange: PropTypes.func,
+       value: PropTypes.string,
+};
+
+export default EventSelect;
 
 import React, { useCallback, useEffect, useRef, useState } from 'react';
 import { Button, Form, ListGroup } from 'react-bootstrap';
 
-import Icon from '../common/Icon';
+import Icon from './Icon';
 import UserBox from '../users/Box';
 import debounce from '../../helpers/debounce';
 
-const UserSelect = ({ name, onChange, value }) => {
+const UserSelect = ({ excludeIds = [], name, onChange, value }) => {
        const [resolved, setResolved] = useState(null);
        const [results, setResults] = useState([]);
        const [search, setSearch] = useState('');
                try {
                        const response = await axios.get(`/api/users`, {
                                params: {
+                                       exclude_ids: excludeIds,
                                        phrase,
                                },
                                signal: ctrl.signal,
                        });
                        ctrl = null;
                        setResults(response.data);
+                       if (phrase) {
+                               setShowResults(true);
+                       }
                } catch (e) {
                        ctrl = null;
                        console.error(e);
                }
-       }, 300), []);
+       }, 300), [excludeIds]);
 
        useEffect(() => {
                fetch(search);
                        </Button>
                </div>;
        }
-       return <div className={`user-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
+       return <div className={`model-select ${showResults ? 'expanded' : 'collapsed'}`} ref={ref}>
                <Form.Control
                        className="search-input"
                        name={Math.random().toString(20).substr(2, 10)}
                                        <ListGroup.Item
                                                action
                                                key={result.id}
-                                               onClick={() => onChange({
-                                                       target: { name, value: result.id },
-                                               })}
+                                               onClick={() => {
+                                                       onChange({
+                                                               target: { name, value: result.id },
+                                                       });
+                                                       setSearch('');
+                                                       setShowResults(false);
+                                               }}
                                        >
                                                <UserBox discriminator noLink user={result} />
                                        </ListGroup.Item>
 };
 
 UserSelect.propTypes = {
+       excludeIds: PropTypes.arrayOf(PropTypes.string),
        name: PropTypes.string,
        onChange: PropTypes.func,
        value: PropTypes.string,
 
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import EventSelect from '../common/EventSelect';
+import Icon from '../common/Icon';
+
+const EventSubscriptions = ({ addEvent, removeEvent, subs }) => {
+       const { t } = useTranslation();
+
+       return <div>
+               <Form.Group controlId="esubs.addEvent">
+                       <Form.Label>{t('discordBot.addEvent')}</Form.Label>
+                       <Form.Control
+                               as={EventSelect}
+                               excludeIds={subs.map(s => s.event_id)}
+                               onChange={e => addEvent(e.target.value)}
+                               value=""
+                       />
+               </Form.Group>
+               {subs.map((esub) => (
+                       <div className="d-flex align-items-center justify-content-between my-2" key={esub.id}>
+                               <div>{esub.event.title}</div>
+                               <div className="button-bar">
+                                       <Button
+                                               onClick={() => removeEvent(esub.event_id)}
+                                               size="sm"
+                                               title={t('button.remove')}
+                                               variant="outline-danger"
+                                       >
+                                               <Icon.DELETE title="" />
+                                       </Button>
+                               </div>
+                       </div>
+               ))}
+       </div>;
+};
+
+EventSubscriptions.propTypes = {
+       addEvent: PropTypes.func,
+       removeEvent: PropTypes.func,
+       subs: PropTypes.arrayOf(PropTypes.shape({
+       })),
+};
+
+export default EventSubscriptions;
 
 import React from 'react';
 import { Col, Row } from 'react-bootstrap';
 import { useTranslation } from 'react-i18next';
+import toastr from 'toastr';
 
+import EventSubscriptions from './EventSubscriptions';
 import GuildProtocol from './GuildProtocol';
+import UserSubscriptions from './UserSubscriptions';
+import ErrorBoundary from '../common/ErrorBoundary';
+import { compareTitle } from '../../helpers/Event';
+import { compareUsername } from '../../helpers/User';
 
 const GuildControls = ({ guild }) => {
        const [protocol, setProtocol] = React.useState([]);
 
        const { t } = useTranslation();
 
+       const addEventSub = React.useCallback(async (event_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               add_event: event_id,
+                       });
+                       setSubscriptions(response.data);
+               } catch (e) {
+                       toastr.error(t('discordBot.eventSubError'));
+               }
+       }, [guild.guild_id, t]);
+
+       const removeEventSub = React.useCallback(async (event_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               remove_event: event_id,
+                       });
+                       setSubscriptions(response.data);
+               } catch (e) {
+                       toastr.error(t('discordBot.eventUnsubError'));
+               }
+       }, [guild.guild_id, t]);
+
+
+       const addUserSub = React.useCallback(async (user_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               add_user: user_id,
+                       });
+                       setSubscriptions(response.data);
+               } catch (e) {
+                       toastr.error(t('discordBot.userSubError'));
+               }
+       }, [guild.guild_id, t]);
+
+       const removeUserSub = React.useCallback(async (user_id) => {
+               try {
+                       const response = await axios.post(`/api/discord-guilds/${guild.guild_id}/subscriptions`, {
+                               remove_user: user_id,
+                       });
+                       setSubscriptions(response.data);
+               } catch (e) {
+                       toastr.error(t('discordBot.userUnsubError'));
+               }
+       }, [guild.guild_id, t]);
+
        React.useEffect(() => {
                const ctrl = new AbortController();
                axios
                axios
                        .get(`/api/discord-guilds/${guild.guild_id}/subscriptions`, { signal: ctrl.signal })
                        .then(response => {
+                               response.data.event_subscriptions.sort((a, b) => {
+                                       return compareTitle(a.event, b.event);
+                               });
+                               response.data.user_subscriptions.sort((a, b) => {
+                                       return compareUsername(a.user, b.user);
+                               });
                                setSubscriptions(response.data);
                        });
                window.Echo.private(`DiscordGuild.${guild.id}`)
                };
        }, [guild.id]);
 
-       return <section className="mt-5">
-               <h2>{t('discordBot.guildControls')}</h2>
-               <Row>
-                       <Col md={6}>
-                               <h3>{t('discordBot.eventSubscriptions')}</h3>
-                               {subscriptions.event_subscriptions ? subscriptions.event_subscriptions.map(esub =>
-                                       <div key={esub.id}>{esub.event.title}</div>
-                               ): null}
-                       </Col>
-                       <Col md={6}>
-                               <h3>{t('discordBot.userSubscriptions')}</h3>
-                               {subscriptions.user_subscriptions ? subscriptions.user_subscriptions.map(usub =>
-                                       <div key={usub.id}>{usub.user.username}</div>
-                               ): null}
-                       </Col>
-               </Row>
-               <h3>{t('discordBot.guildProtocol')}</h3>
-               <GuildProtocol protocol={protocol} />
-       </section>;
+       return <>
+               <section className="mt-5">
+                       <h2>{t('discordBot.guildControls')}</h2>
+                       <Row>
+                               <Col md={6}>
+                                       <h3>{t('discordBot.eventSubscriptions')}</h3>
+                                       <ErrorBoundary>
+                                               <EventSubscriptions
+                                                       addEvent={addEventSub}
+                                                       removeEvent={removeEventSub}
+                                                       subs={subscriptions.event_subscriptions || []}
+                                               />
+                                       </ErrorBoundary>
+                               </Col>
+                               <Col md={6}>
+                                       <h3>{t('discordBot.userSubscriptions')}</h3>
+                                       <ErrorBoundary>
+                                               <UserSubscriptions
+                                                       addUser={addUserSub}
+                                                       removeUser={removeUserSub}
+                                                       subs={subscriptions.user_subscriptions || []}
+                                               />
+                                       </ErrorBoundary>
+                               </Col>
+                       </Row>
+               </section>
+               <section className="mt-5">
+                       <h3>{t('discordBot.guildProtocol')}</h3>
+                       <GuildProtocol protocol={protocol} />
+               </section>
+       </>;
 };
 
 GuildControls.propTypes = {
 
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button, Form } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
+
+import UserSelect from '../common/UserSelect';
+import Icon from '../common/Icon';
+import UserBox from '../users/Box';
+
+const UserSubscriptions = ({ addUser, removeUser, subs }) => {
+       const { t } = useTranslation();
+
+       return <div>
+               <Form.Group controlId="usubs.addUser">
+                       <Form.Label>{t('discordBot.addUser')}</Form.Label>
+                       <Form.Control
+                               as={UserSelect}
+                               excludeIds={subs.map(s => s.user_id)}
+                               onChange={e => addUser(e.target.value)}
+                               value=""
+                       />
+               </Form.Group>
+               {subs.map((usub) => (
+                       <div className="d-flex align-items-center justify-content-between my-2" key={usub.id}>
+                               <UserBox user={usub.user} />
+                               <div className="button-bar">
+                                       <Button
+                                               onClick={() => removeUser(usub.user_id)}
+                                               size="sm"
+                                               title={t('button.remove')}
+                                               variant="outline-danger"
+                                       >
+                                               <Icon.DELETE title="" />
+                                       </Button>
+                               </div>
+                       </div>
+               ))}
+       </div>;
+};
+
+UserSubscriptions.propTypes = {
+       addUser: PropTypes.func,
+       removeUser: PropTypes.func,
+       subs: PropTypes.arrayOf(PropTypes.shape({
+       })),
+};
+
+export default UserSubscriptions;
 
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+
+const Box = ({ event }) => {
+       return <div className="event-box">
+               <span>{event.title}</span>
+       </div>;
+};
+
+Box.propTypes = {
+       event: PropTypes.shape({
+               title: PropTypes.string,
+       }),
+};
+
+export default Box;
 
 import moment from 'moment';
 
+import { getTranslation } from './Technique';
+import i18n from '../i18n';
+
 export const getLink = event => `/events/${event.name}`;
 
 export const hasConcluded = event => event && event.end && moment(event.end).isBefore(moment());
        }
        return 0;
 };
+
+const getTitle = (event) =>
+       (event.description && getTranslation(event.description, 'title', i18n.language))
+               || event.title;
+
+export const compareTitle = (a, b) => {
+       const a_title = getTitle(a);
+       const b_title = getTitle(b);
+       return a_title.localeCompare(b_title);
+};
 
                        },
                },
                discordBot: {
+                       addEvent: 'Event abonnieren',
+                       addUser: 'User abonnieren',
                        channel: 'Kanal',
                        channelControls: 'Kanal-Steuerung',
                        commandStatus: {
                                result: 'Ergebnis',
                        },
                        controls: 'Steuerung',
+                       eventSubError: 'Fehler bim Abonnieren',
                        eventSubscriptions: 'Event Subscriptions',
+                       eventUnsubError: 'Fehler beim Kündigen',
                        guild: 'Server',
                        guildControls: 'Server-Steuerung',
                        guildProtocol: 'Command Protokoll',
                        messageSuccess: 'Nachricht in Warteschlange',
                        selectGuild: 'Bitte Server wählen',
                        sendMessage: 'Nachricht senden',
+                       userSubError: 'Fehler beim Abonnieren',
                        userSubscriptions: 'User Subscriptions',
+                       userUnsubError: 'Fehler beim Kündigen',
                },
                episodes: {
                        addRestream: 'Neuer Restream',
 
                        },
                },
                discordBot: {
+                       addEvent: 'Subscribe to event',
+                       addUser: 'Subscribe to user',
                        channel: 'Channel',
                        channelControls: 'Channel controls',
                        commandStatus: {
                                result: 'Result',
                        },
                        controls: 'Controls',
+                       eventSubError: 'Error subscribing',
                        eventSubscriptions: 'Event subscriptions',
+                       eventUnsubError: 'Error unsubscribing',
                        guild: 'Server',
                        guildControls: 'Server controls',
                        guildProtocol: 'Command protocol',
                        messageSuccess: 'Message queued',
                        selectGuild: 'Please select server',
                        sendMessage: 'Send message',
+                       userSubError: 'Error subscribing',
                        userSubscriptions: 'User subscriptions',
+                       userUnsubError: 'Error unsubscribing',
                },
                episodes: {
                        addRestream: 'Add Restream',
 
-.discord-select {
-       .search-results-holder {
-               position: relative;
-       }
-       .search-results {
-               position: absolute;
-               left: 0;
-               top: 100%;
-               z-index: 1;
-               width: 100%;
-               border-top-left-radius: 0;
-               border-top-right-radius: 0;
-               box-shadow: 1ex 1ex 1ex rgba(0, 0, 0, 0.5);
-       }
-       &.collapsed .search-results {
-               display: none;
-       }
-       &.expanded .search-input {
-               border-bottom-left-radius: 0;
-               border-bottom-right-radius: 0;
-       }
-}
-
 .channel-box {
        > svg {
                margin-right: 0.25rem;
 
                }
        }
 }
+
+.model-select {
+       .search-results-holder {
+               position: relative;
+       }
+       .search-results {
+               position: absolute;
+               top: 100%;
+               left: 0;
+               z-index: 4; /* active pagination links have z-index 3 for some reason */
+               width: 100%;
+               border-top-left-radius: 0;
+               border-top-right-radius: 0;
+               box-shadow: 1ex 1ex 1ex rgba(0, 0, 0, 0.5);
+       }
+
+       &.collapsed {
+               .search-results {
+                       display: none;
+               }
+       }
+       &.expanded {
+               .search-input {
+                       border-bottom-left-radius: 0;
+                       border-bottom-right-radius: 0;
+               }
+       }
+}
 
 Route::get('discord-guilds/{guild_id}', 'App\Http\Controllers\DiscordGuildController@single');
 Route::get('discord-guilds/{guild_id}/channels', 'App\Http\Controllers\DiscordChannelController@search');
 Route::get('discord-guilds/{guild_id}/subscriptions', 'App\Http\Controllers\DiscordGuildController@subscriptions');
+Route::post('discord-guilds/{guild_id}/subscriptions', 'App\Http\Controllers\DiscordGuildController@manageSubscriptions');
 
 Route::get('episodes', 'App\Http\Controllers\EpisodeController@search');
 Route::post('episodes/{episode}/add-restream', 'App\Http\Controllers\EpisodeController@addRestream');