diff --git a/js/src/components/Event/AddressAutoComplete.vue b/js/src/components/Event/AddressAutoComplete.vue
index 5dc47ea5b..76197a2d0 100644
--- a/js/src/components/Event/AddressAutoComplete.vue
+++ b/js/src/components/Event/AddressAutoComplete.vue
@@ -81,11 +81,12 @@ import { Modal } from 'buefy/dist/components/dialog';
 export default class AddressAutoComplete extends Vue {
 
   @Prop({ required: false, default: () => [] }) initialData!: IAddress[];
+  @Prop({ required: false }) value!: IAddress;
 
   data: IAddress[] = this.initialData;
   selected: IAddress|null = new Address();
   isFetching: boolean = false;
-  queryText: string = '';
+  queryText: string = this.value && this.value.description || '';
   addressModalActive: boolean = false;
 
   async getAsyncData(query) {
diff --git a/js/src/components/Event/TagInput.vue b/js/src/components/Event/TagInput.vue
index 0a5f556df..385f4d38f 100644
--- a/js/src/components/Event/TagInput.vue
+++ b/js/src/components/Event/TagInput.vue
@@ -1,33 +1,51 @@
 <template>
     <b-field label="Enter some tags">
         <b-taginput
-                v-model="tags"
+                v-model="tagsStrings"
                 :data="filteredTags"
                 autocomplete
                 :allow-new="true"
                 :field="path"
                 icon="label"
                 placeholder="Add a tag"
-                @typing="getFilteredTags">
+                @typing="getFilteredTags"
+        >
         </b-taginput>
     </b-field>
 </template>
 <script lang="ts">
-import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
-import { get } from 'lodash';
+import { Component, Prop, Vue } from 'vue-property-decorator';
+import { get, differenceBy } from 'lodash';
 import { ITag } from '@/types/tag.model';
-@Component
+
+@Component({
+  computed: {
+    tagsStrings: {
+      get() {
+        return this.$props.data.map((tag: ITag) => tag.title);
+      },
+      set(tagStrings) {
+        const tagEntities = tagStrings.map((tag) => {
+          if (TagInput.isTag(tag)) {
+            return tag;
+          }
+          return { title: tag, slug: tag } as ITag;
+        });
+        this.$emit('input', tagEntities);
+      },
+    },
+  },
+})
 export default class TagInput extends Vue {
 
-  @Prop({ required: false, default: () => [] }) data!: object[];
+  @Prop({ required: false, default: () => [] }) data!: ITag[];
   @Prop({ required: true, default: 'value' }) path!: string;
-  @Prop({ required: true }) value!: string;
+  @Prop({ required: true }) value!: ITag[];
 
-  filteredTags: object[] = [];
-  tags: object[] = [];
+  filteredTags: ITag[] = [];
 
   getFilteredTags(text) {
-    this.filteredTags = this.data.filter((option) => {
+    this.filteredTags = differenceBy(this.data, this.value, 'id').filter((option) => {
       return get(option, this.path)
                 .toString()
                 .toLowerCase()
@@ -35,18 +53,6 @@ export default class TagInput extends Vue {
     });
   }
 
-  @Watch('tags')
-  onTagsChanged (tags) {
-    const tagEntities = tags.map((tag) => {
-      if (TagInput.isTag(tag)) {
-        return tag;
-      }
-      return { title: tag, slug: tag } as ITag;
-    });
-    console.log('tags changed', tagEntities);
-    this.$emit('input', tagEntities);
-  }
-
   static isTag(x: any): x is ITag {
     return x.slug !== undefined;
   }
diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts
index a2374bb15..eac64045d 100644
--- a/js/src/graphql/event.ts
+++ b/js/src/graphql/event.ts
@@ -52,6 +52,7 @@ export const FETCH_EVENT = gql`
         domain,
         name,
         url,
+        id,
       },
       # attributedTo {
       #     avatar {
@@ -64,6 +65,7 @@ export const FETCH_EVENT = gql`
         ${participantQuery}
       },
       tags {
+        id,
         slug,
         title
       },
@@ -82,6 +84,25 @@ export const FETCH_EVENT = gql`
           domain,
           name,
         }
+      },
+      options {
+        maximumAttendeeCapacity,
+        remainingAttendeeCapacity,
+        showRemainingAttendeeCapacity,
+        offers {
+          price,
+          priceCurrency,
+          url
+        },
+        participationConditions {
+          title,
+          content,
+          url
+        },
+        attendees,
+        program,
+        commentModeration,
+        showParticipationPrice
       }
     }
   }
@@ -144,6 +165,7 @@ export const CREATE_EVENT = gql`
     $organizerActorId: ID!,
     $category: String,
     $beginsOn: DateTime!,
+    $endsOn: DateTime,
     $picture: PictureInput,
     $tags: [String],
     $options: EventOptionsInput,
@@ -154,6 +176,7 @@ export const CREATE_EVENT = gql`
       title: $title,
       description: $description,
       beginsOn: $beginsOn,
+      endsOn: $endsOn,
       organizerActorId: $organizerActorId,
       category: $category,
       options: $options,
@@ -173,13 +196,32 @@ export const CREATE_EVENT = gql`
 `;
 
 export const EDIT_EVENT = gql`
-  mutation EditEvent(
+  mutation updateEvent(
+  $id: ID!,
   $title: String!,
   $description: String!,
-  $organizerActorId: Int!,
-  $category: String
+  $organizerActorId: ID!,
+  $category: String,
+  $beginsOn: DateTime!,
+  $endsOn: DateTime,
+  $picture: PictureInput,
+  $tags: [String],
+  $options: EventOptionsInput,
+  $physicalAddress: AddressInput,
+  $visibility: EventVisibility
   ) {
-    EditEvent(title: $title, description: $description, organizerActorId: $organizerActorId, category: $category) {
+    updateEvent(eventId: $id,
+        title: $title,
+        description: $description,
+        beginsOn: $beginsOn,
+        endsOn: $endsOn,
+        organizerActorId: $organizerActorId,
+        category: $category,
+        options: $options,
+        picture: $picture,
+        tags: $tags,
+        physicalAddress: $physicalAddress,
+        visibility: $visibility) {
       uuid
     }
   }
diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts
index d468b6299..b92244dde 100644
--- a/js/src/types/event.model.ts
+++ b/js/src/types/event.model.ts
@@ -4,9 +4,9 @@ import { ITag } from '@/types/tag.model';
 import { IPicture } from '@/types/picture.model';
 
 export enum EventStatus {
-  TENTATIVE,
-  CONFIRMED,
-  CANCELLED,
+  TENTATIVE = 'TENTATIVE',
+  CONFIRMED = 'CONFIRMED',
+  CANCELLED = 'CANCELLED',
 }
 
 export enum EventVisibility {
@@ -17,9 +17,9 @@ export enum EventVisibility {
 }
 
 export enum EventJoinOptions {
-  FREE,
-  RESTRICTED,
-  INVITE,
+  FREE = 'FREE',
+  RESTRICTED = 'RESTRICTED',
+  INVITE = 'INVITE',
 }
 
 export enum EventVisibilityJoinOptions {
diff --git a/js/src/views/Event/Edit.vue b/js/src/views/Event/Edit.vue
index b8f24d3c0..56bf130b9 100644
--- a/js/src/views/Event/Edit.vue
+++ b/js/src/views/Event/Edit.vue
@@ -177,7 +177,7 @@
 <script lang="ts">
 import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
 import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
-import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, IEvent, CommentModeration } from '@/types/event.model';
+import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration } from '@/types/event.model';
 import { LOGGED_PERSON } from '@/graphql/actor';
 import { IPerson, Person } from '@/types/actor';
 import PictureUpload from '@/components/PictureUpload.vue';
@@ -207,6 +207,7 @@ export default class EditEvent extends Vue {
   eventId!: string | undefined;
 
   loggedPerson = new Person();
+  tags: ITag[] = [];
   event = new EventModel();
   pictureFile: File | null = null;
 
@@ -223,7 +224,7 @@ export default class EditEvent extends Vue {
 
   @Watch('$route.params.eventId', { immediate: true })
   async onEventIdParamChanged (val: string) {
-    if (this.isUpdate !== true) return;
+    if (!this.isUpdate) return;
 
     this.eventId = val;
 
@@ -231,6 +232,7 @@ export default class EditEvent extends Vue {
       this.event = await this.getEvent();
 
       this.pictureFile = await buildFileFromIPicture(this.event.picture);
+      this.limitedPlaces = this.event.options.maximumAttendeeCapacity != null;
     }
   }
 
@@ -241,7 +243,6 @@ export default class EditEvent extends Vue {
 
     this.event.beginsOn = now;
     this.event.endsOn = end;
-    console.log('eventvisibilityjoinoptions', this.eventVisibilityJoinOptions);
   }
 
   createOrUpdate(e: Event) {
@@ -261,7 +262,7 @@ export default class EditEvent extends Vue {
 
       console.log('Event created', data);
 
-      this.$router.push({
+      await this.$router.push({
         name: 'Event',
         params: { uuid: data.createEvent.uuid },
       });
@@ -277,7 +278,7 @@ export default class EditEvent extends Vue {
         variables: this.buildVariables(),
       });
 
-      this.$router.push({
+      await this.$router.push({
         name: 'Event',
         params: { uuid: this.eventId as string },
       });
@@ -297,6 +298,8 @@ export default class EditEvent extends Vue {
     };
     const res = Object.assign({}, this.event, obj);
 
+    delete this.event.options['__typename'];
+
     if (this.event.physicalAddress) {
       delete this.event.physicalAddress['__typename'];
     }
diff --git a/js/src/views/Event/Event.vue b/js/src/views/Event/Event.vue
index efaee349a..15589877e 100644
--- a/js/src/views/Event/Event.vue
+++ b/js/src/views/Event/Event.vue
@@ -57,7 +57,7 @@
                 <p class="control">
                   <router-link
                           class="button"
-                          :to="{ name: 'EditEvent', params: {uuid: event.uuid}}"
+                          :to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
                   >
                     <translate>Edit</translate>
                   </router-link>
diff --git a/lib/mobilizon/actors/actor.ex b/lib/mobilizon/actors/actor.ex
index ba4477e8f..2cf983479 100644
--- a/lib/mobilizon/actors/actor.ex
+++ b/lib/mobilizon/actors/actor.ex
@@ -110,6 +110,24 @@ defmodule Mobilizon.Actors.Actor do
     |> unique_constraint(:url, name: :actors_url_index)
   end
 
+  @doc false
+  def update_changeset(%Actor{} = actor, attrs) do
+    actor
+    |> Ecto.Changeset.cast(attrs, [
+      :name,
+      :summary,
+      :keys,
+      :manually_approves_followers,
+      :suspended,
+      :user_id
+    ])
+    |> cast_embed(:avatar)
+    |> cast_embed(:banner)
+    |> validate_required([:preferred_username, :keys, :suspended, :url])
+    |> unique_constraint(:preferred_username, name: :actors_preferred_username_domain_type_index)
+    |> unique_constraint(:url, name: :actors_url_index)
+  end
+
   @doc """
   Changeset for person registration
   """
diff --git a/lib/mobilizon/actors/actors.ex b/lib/mobilizon/actors/actors.ex
index 5cd0e490b..7ab1a9b0b 100644
--- a/lib/mobilizon/actors/actors.ex
+++ b/lib/mobilizon/actors/actors.ex
@@ -124,7 +124,7 @@ defmodule Mobilizon.Actors do
   """
   def update_actor(%Actor{} = actor, attrs) do
     actor
-    |> Actor.changeset(attrs)
+    |> Actor.update_changeset(attrs)
     |> delete_files_if_media_changed()
     |> Repo.update()
   end
diff --git a/lib/mobilizon/events/event.ex b/lib/mobilizon/events/event.ex
index 3521ede04..52d1c765d 100644
--- a/lib/mobilizon/events/event.ex
+++ b/lib/mobilizon/events/event.ex
@@ -54,10 +54,10 @@ defmodule Mobilizon.Events.Event do
     field(:online_address, :string)
     field(:phone_address, :string)
     field(:category, :string)
-    embeds_one(:options, Mobilizon.Events.EventOptions)
+    embeds_one(:options, Mobilizon.Events.EventOptions, on_replace: :update)
     belongs_to(:organizer_actor, Actor, foreign_key: :organizer_actor_id)
     belongs_to(:attributed_to, Actor, foreign_key: :attributed_to_id)
-    many_to_many(:tags, Tag, join_through: "events_tags")
+    many_to_many(:tags, Tag, join_through: "events_tags", on_replace: :delete)
     many_to_many(:participants, Actor, join_through: Participant)
     has_many(:tracks, Track)
     has_many(:sessions, Session)
@@ -98,6 +98,38 @@ defmodule Mobilizon.Events.Event do
     ])
   end
 
+  @doc false
+  def update_changeset(%Event{} = event, attrs) do
+    event
+    |> Ecto.Changeset.cast(attrs, [
+      :title,
+      :slug,
+      :description,
+      :begins_on,
+      :ends_on,
+      :category,
+      :status,
+      :visibility,
+      :publish_at,
+      :online_address,
+      :phone_address,
+      :picture_id,
+      :physical_address_id
+    ])
+    |> cast_embed(:options)
+    |> put_tags(attrs)
+    |> validate_required([
+      :title,
+      :begins_on,
+      :organizer_actor_id,
+      :url,
+      :uuid
+    ])
+  end
+
+  defp put_tags(changeset, %{"tags" => tags}), do: put_assoc(changeset, :tags, tags)
+  defp put_tags(changeset, _), do: changeset
+
   def can_event_be_managed_by(%Event{organizer_actor_id: organizer_actor_id}, actor_id)
       when organizer_actor_id == actor_id do
     {:event_can_be_managed, true}
diff --git a/lib/mobilizon/events/events.ex b/lib/mobilizon/events/events.ex
index ccb198339..cc6d71702 100644
--- a/lib/mobilizon/events/events.ex
+++ b/lib/mobilizon/events/events.ex
@@ -227,11 +227,12 @@ defmodule Mobilizon.Events do
            :tracks,
            :tags,
            :participants,
-           :physical_address
+           :physical_address,
+           :picture
          ])}
 
-      err ->
-        {:error, err}
+      _err ->
+        {:error, :event_not_found}
     end
   end
 
@@ -435,7 +436,8 @@ defmodule Mobilizon.Events do
   """
   def update_event(%Event{} = event, attrs) do
     event
-    |> Event.changeset(attrs)
+    |> Repo.preload(:tags)
+    |> Event.update_changeset(attrs)
     |> Repo.update()
   end
 
diff --git a/lib/mobilizon_web/api/events.ex b/lib/mobilizon_web/api/events.ex
index 5e138eef8..f841d2597 100644
--- a/lib/mobilizon_web/api/events.ex
+++ b/lib/mobilizon_web/api/events.ex
@@ -2,8 +2,7 @@ defmodule MobilizonWeb.API.Events do
   @moduledoc """
   API for Events
   """
-  alias Mobilizon.Actors
-  alias Mobilizon.Actors.Actor
+  alias Mobilizon.Events.Event
   alias Mobilizon.Service.ActivityPub
   alias Mobilizon.Service.ActivityPub.Utils, as: ActivityPubUtils
   alias MobilizonWeb.API.Utils
@@ -12,28 +11,23 @@ defmodule MobilizonWeb.API.Events do
   Create an event
   """
   @spec create_event(map()) :: {:ok, Activity.t(), Event.t()} | any()
-  def create_event(
-        %{
-          begins_on: begins_on,
-          description: description,
-          options: options,
-          organizer_actor_id: organizer_actor_id,
-          tags: tags,
-          title: title
-        } = args
-      )
-      when is_map(options) do
-    with %Actor{url: url} = actor <-
-           Actors.get_local_actor_with_everything(organizer_actor_id),
-         physical_address <- Map.get(args, :physical_address, nil),
-         title <- String.trim(title),
-         visibility <- Map.get(args, :visibility, :public),
-         picture <- Map.get(args, :picture, nil),
-         {content_html, tags, to, cc} <-
-           Utils.prepare_content(actor, description, visibility, tags, nil),
+  def create_event(%{organizer_actor: organizer_actor} = args) do
+    with %{
+           title: title,
+           physical_address: physical_address,
+           picture: picture,
+           content_html: content_html,
+           tags: tags,
+           to: to,
+           cc: cc,
+           begins_on: begins_on,
+           ends_on: ends_on,
+           category: category,
+           options: options
+         } <- prepare_args(args),
          event <-
            ActivityPubUtils.make_event_data(
-             url,
+             organizer_actor.url,
              %{to: to, cc: cc},
              title,
              content_html,
@@ -41,17 +35,104 @@ defmodule MobilizonWeb.API.Events do
              tags,
              %{
                begins_on: begins_on,
+               ends_on: ends_on,
                physical_address: physical_address,
-               category: Map.get(args, :category),
+               category: category,
                options: options
              }
            ) do
       ActivityPub.create(%{
         to: ["https://www.w3.org/ns/activitystreams#Public"],
-        actor: actor,
+        actor: organizer_actor,
         object: event,
         local: true
       })
     end
   end
+
+  @doc """
+  Update an event
+  """
+  @spec update_event(map(), Event.t()) :: {:ok, Activity.t(), Event.t()} | any()
+  def update_event(
+        %{
+          organizer_actor: organizer_actor
+        } = args,
+        %Event{} = event
+      ) do
+    with args <- Map.put(args, :tags, Map.get(args, :tags, [])),
+         %{
+           title: title,
+           physical_address: physical_address,
+           picture: picture,
+           content_html: content_html,
+           tags: tags,
+           to: to,
+           cc: cc,
+           begins_on: begins_on,
+           ends_on: ends_on,
+           category: category,
+           options: options
+         } <-
+           prepare_args(Map.merge(event, args)),
+         event <-
+           ActivityPubUtils.make_event_data(
+             organizer_actor.url,
+             %{to: to, cc: cc},
+             title,
+             content_html,
+             picture,
+             tags,
+             %{
+               begins_on: begins_on,
+               ends_on: ends_on,
+               physical_address: physical_address,
+               category: category,
+               options: options
+             },
+             event.uuid,
+             event.url
+           ) do
+      ActivityPub.update(%{
+        to: ["https://www.w3.org/ns/activitystreams#Public"],
+        actor: organizer_actor.url,
+        cc: [],
+        object: event,
+        local: true
+      })
+    end
+  end
+
+  defp prepare_args(
+         %{
+           organizer_actor: organizer_actor,
+           title: title,
+           description: description,
+           options: options,
+           tags: tags,
+           begins_on: begins_on,
+           category: category
+         } = args
+       ) do
+    with physical_address <- Map.get(args, :physical_address, nil),
+         title <- String.trim(title),
+         visibility <- Map.get(args, :visibility, :public),
+         picture <- Map.get(args, :picture, nil),
+         {content_html, tags, to, cc} <-
+           Utils.prepare_content(organizer_actor, description, visibility, tags, nil) do
+      %{
+        title: title,
+        physical_address: physical_address,
+        picture: picture,
+        content_html: content_html,
+        tags: tags,
+        to: to,
+        cc: cc,
+        begins_on: begins_on,
+        ends_on: Map.get(args, :ends_on, nil),
+        category: category,
+        options: options
+      }
+    end
+  end
 end
diff --git a/lib/mobilizon_web/api/groups.ex b/lib/mobilizon_web/api/groups.ex
index 5033792b3..7ff870670 100644
--- a/lib/mobilizon_web/api/groups.ex
+++ b/lib/mobilizon_web/api/groups.ex
@@ -18,13 +18,13 @@ defmodule MobilizonWeb.API.Groups do
           preferred_username: title,
           summary: summary,
           creator_actor_id: creator_actor_id,
-          avatar: avatar,
-          banner: banner
+          avatar: _avatar,
+          banner: _banner
         } = args
       ) do
     with {:is_owned, true, actor} <- User.owns_actor(user, creator_actor_id),
-         {:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
          title <- String.trim(title),
+         {:existing_group, nil} <- {:existing_group, Actors.get_group_by_title(title)},
          visibility <- Map.get(args, :visibility, :public),
          {content_html, tags, to, cc} <-
            Utils.prepare_content(actor, summary, visibility, [], nil),
diff --git a/lib/mobilizon_web/resolvers/event.ex b/lib/mobilizon_web/resolvers/event.ex
index 8a01b2cf0..8eadf44a6 100644
--- a/lib/mobilizon_web/resolvers/event.ex
+++ b/lib/mobilizon_web/resolvers/event.ex
@@ -58,7 +58,8 @@ defmodule MobilizonWeb.Resolvers.Event do
       ) do
     # We get the organizer's next public event
     events =
-      [Events.get_actor_upcoming_public_event(organizer_actor, uuid)] |> Enum.filter(&is_map/1)
+      [Events.get_actor_upcoming_public_event(organizer_actor, uuid)]
+      |> Enum.filter(&is_map/1)
 
     # We find similar events with the same tags
     # uniq_by : It's possible event_from_same_actor is inside events_from_tags
@@ -150,7 +151,17 @@ defmodule MobilizonWeb.Resolvers.Event do
          {:has_event, {:ok, %Event{} = event}} <-
            {:has_event, Mobilizon.Events.get_event_full(event_id)},
          {:ok, _activity, _participant} <- MobilizonWeb.API.Participations.leave(event, actor) do
-      {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
+      {
+        :ok,
+        %{
+          event: %{
+            id: event_id
+          },
+          actor: %{
+            id: actor_id
+          }
+        }
+      }
     else
       {:has_event, _} ->
         {:error, "Event with this ID #{inspect(event_id)} doesn't exist"}
@@ -173,12 +184,35 @@ defmodule MobilizonWeb.Resolvers.Event do
   @doc """
   Create an event
   """
-  def create_event(_parent, args, %{context: %{current_user: _user}} = _resolution) do
-    with {:ok, args} <- save_attached_picture(args),
+  def create_event(
+        _parent,
+        %{organizer_actor_id: organizer_actor_id} = args,
+        %{
+          context: %{
+            current_user: user
+          }
+        } = _resolution
+      ) do
+    # See https://github.com/absinthe-graphql/absinthe/issues/490
+    with args <- Map.put(args, :options, args[:options] || %{}),
+         {:is_owned, true, organizer_actor} <- User.owns_actor(user, organizer_actor_id),
+         {:ok, args} <- save_attached_picture(args),
          {:ok, args} <- save_physical_address(args),
-         {:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <-
-           MobilizonWeb.API.Events.create_event(args) do
+         args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
+         {
+           :ok,
+           %Activity{
+             data: %{
+               "object" => %{"type" => "Event"} = _object
+             }
+           },
+           %Event{} = event
+         } <-
+           MobilizonWeb.API.Events.create_event(args_with_organizer) do
       {:ok, event}
+    else
+      {:is_owned, false} ->
+        {:error, "Organizer actor id is not owned by the user"}
     end
   end
 
@@ -186,19 +220,72 @@ defmodule MobilizonWeb.Resolvers.Event do
     {:error, "You need to be logged-in to create events"}
   end
 
+  @doc """
+  Update an event
+  """
+  def update_event(
+        _parent,
+        %{event_id: event_id} = args,
+        %{
+          context: %{
+            current_user: user
+          }
+        } = _resolution
+      ) do
+    # See https://github.com/absinthe-graphql/absinthe/issues/490
+    with args <- Map.put(args, :options, args[:options] || %{}),
+         {:ok, %Event{} = event} <- Mobilizon.Events.get_event_full(event_id),
+         {:is_owned, true, organizer_actor} <- User.owns_actor(user, event.organizer_actor_id),
+         {:ok, args} <- save_attached_picture(args),
+         {:ok, args} <- save_physical_address(args),
+         args <- Map.put(args, :organizer_actor, organizer_actor),
+         {
+           :ok,
+           %Activity{
+             data: %{
+               "object" => %{"type" => "Event"} = _object
+             }
+           },
+           %Event{} = event
+         } <-
+           MobilizonWeb.API.Events.update_event(args, event) do
+      {:ok, event}
+    else
+      {:error, :event_not_found} ->
+        {:error, "Event not found"}
+
+      {:is_owned, _} ->
+        {:error, "User doesn't own actor"}
+    end
+  end
+
+  def update_event(_parent, _args, _resolution) do
+    {:error, "You need to be logged-in to update an event"}
+  end
+
   # If we have an attached picture, just transmit it. It will be handled by
   # Mobilizon.Service.ActivityPub.Utils.make_picture_data/1
   # However, we need to pass it's actor ID
   @spec save_attached_picture(map()) :: {:ok, map()}
   defp save_attached_picture(
-         %{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
+         %{
+           picture: %{
+             picture: %{file: %Plug.Upload{} = _picture} = all_pic
+           }
+         } = args
        ) do
     {:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))}
   end
 
   # Otherwise if we use a previously uploaded picture we need to fetch it from database
   @spec save_attached_picture(map()) :: {:ok, map()}
-  defp save_attached_picture(%{picture: %{picture_id: picture_id}} = args) do
+  defp save_attached_picture(
+         %{
+           picture: %{
+             picture_id: picture_id
+           }
+         } = args
+       ) do
     with %Picture{} = picture <- Mobilizon.Media.get_picture(picture_id) do
       {:ok, Map.put(args, :picture, picture)}
     end
@@ -208,7 +295,13 @@ defmodule MobilizonWeb.Resolvers.Event do
   defp save_attached_picture(args), do: {:ok, args}
 
   @spec save_physical_address(map()) :: {:ok, map()}
-  defp save_physical_address(%{physical_address: %{url: physical_address_url}} = args)
+  defp save_physical_address(
+         %{
+           physical_address: %{
+             url: physical_address_url
+           }
+         } = args
+       )
        when not is_nil(physical_address_url) do
     with %Address{} = address <- Addresses.get_address_by_url(physical_address_url),
          args <- Map.put(args, :physical_address, address.url) do
@@ -230,9 +323,15 @@ defmodule MobilizonWeb.Resolvers.Event do
   @doc """
   Delete an event
   """
-  def delete_event(_parent, %{event_id: event_id, actor_id: actor_id}, %{
-        context: %{current_user: user}
-      }) do
+  def delete_event(
+        _parent,
+        %{event_id: event_id, actor_id: actor_id},
+        %{
+          context: %{
+            current_user: user
+          }
+        }
+      ) do
     with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id),
          {:is_owned, true, _} <- User.owns_actor(user, actor_id),
          {:event_can_be_managed, true} <- Event.can_event_be_managed_by(event, actor_id),
diff --git a/lib/mobilizon_web/schema/event.ex b/lib/mobilizon_web/schema/event.ex
index 8cbed015e..f5d2020b3 100644
--- a/lib/mobilizon_web/schema/event.ex
+++ b/lib/mobilizon_web/schema/event.ex
@@ -229,11 +229,42 @@ defmodule MobilizonWeb.Schema.EventType do
       arg(:organizer_actor_id, non_null(:id))
       arg(:category, :string, default_value: "meeting")
       arg(:physical_address, :address_input)
-      arg(:options, :event_options_input, default_value: %{})
+      arg(:options, :event_options_input)
 
       resolve(&Event.create_event/3)
     end
 
+    @desc "Update an event"
+    field :update_event, type: :event do
+      arg(:event_id, non_null(:id))
+
+      arg(:title, :string)
+      arg(:description, :string)
+      arg(:begins_on, :datetime)
+      arg(:ends_on, :datetime)
+      arg(:state, :integer)
+      arg(:status, :integer)
+      arg(:public, :boolean)
+      arg(:visibility, :event_visibility)
+      arg(:organizer_actor_id, :id)
+
+      arg(:tags, list_of(:string), description: "The list of tags associated to the event")
+
+      arg(:picture, :picture_input,
+        description:
+          "The picture for the event, either as an object or directly the ID of an existing Picture"
+      )
+
+      arg(:publish_at, :datetime)
+      arg(:online_address, :string)
+      arg(:phone_address, :string)
+      arg(:category, :string)
+      arg(:physical_address, :address_input)
+      arg(:options, :event_options_input)
+
+      resolve(&Event.update_event/3)
+    end
+
     @desc "Delete an event"
     field :delete_event, :deleted_object do
       arg(:event_id, non_null(:integer))
diff --git a/lib/service/activity_pub/activity_pub.ex b/lib/service/activity_pub/activity_pub.ex
index 3823d7c33..6cad45aab 100644
--- a/lib/service/activity_pub/activity_pub.ex
+++ b/lib/service/activity_pub/activity_pub.ex
@@ -39,24 +39,16 @@ defmodule Mobilizon.Service.ActivityPub do
   @doc """
   Wraps an object into an activity
   """
-  # TODO: Rename me
-  @spec insert(map(), boolean()) :: {:ok, %Activity{}} | {:error, any()}
-  def insert(map, local \\ true) when is_map(map) do
-    with map <- lazy_put_activity_defaults(map),
-         {:ok, object} <- insert_full_object(map) do
-      activity = %Activity{
-        data: map,
-        local: local,
-        actor: map["actor"],
-        recipients: get_recipients(map)
-      }
-
-      # Notification.create_notifications(activity)
-      # stream_out(activity)
-      {:ok, activity, object}
-    else
-      %Activity{} = activity -> {:ok, activity}
-      error -> {:error, error}
+  @spec create_activity(map(), boolean()) :: {:ok, %Activity{}}
+  def create_activity(map, local \\ true) when is_map(map) do
+    with map <- lazy_put_activity_defaults(map) do
+      {:ok,
+       %Activity{
+         data: map,
+         local: local,
+         actor: map["actor"],
+         recipients: get_recipients(map)
+       }}
     end
   end
 
@@ -137,7 +129,8 @@ defmodule Mobilizon.Service.ActivityPub do
              %{to: to, actor: actor, published: published, object: object},
              additional
            ),
-         {:ok, activity, object} <- insert(create_data, local),
+         {:ok, activity} <- create_activity(create_data, local),
+         {:ok, object} <- insert_full_object(create_data),
          :ok <- maybe_federate(activity) do
       # {:ok, actor} <- Actors.increase_event_count(actor) do
       {:ok, activity, object}
@@ -160,7 +153,8 @@ defmodule Mobilizon.Service.ActivityPub do
            "object" => object,
            "id" => activity_wrapper_id || get_url(object) <> "/activity"
          },
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- insert_full_object(data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     end
@@ -177,7 +171,8 @@ defmodule Mobilizon.Service.ActivityPub do
            "object" => object,
            "id" => activity_wrapper_id || get_url(object) <> "/activity"
          },
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- insert_full_object(data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     end
@@ -195,7 +190,8 @@ defmodule Mobilizon.Service.ActivityPub do
            "actor" => actor,
            "object" => object
          },
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- update_object(object["id"], data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     end
@@ -210,7 +206,8 @@ defmodule Mobilizon.Service.ActivityPub do
   #     ) do
   #   with nil <- get_existing_like(url, object),
   #        like_data <- make_like_data(user, object, activity_id),
-  #        {:ok, activity, object} <- insert(like_data, local),
+  #        {:ok, activity} <- create_activity(like_data, local),
+  #        {:ok, object} <- insert_full_object(data),
   #        {:ok, object} <- add_like_to_object(activity, object),
   #        :ok <- maybe_federate(activity) do
   #     {:ok, activity, object}
@@ -228,7 +225,8 @@ defmodule Mobilizon.Service.ActivityPub do
   #     ) do
   #   with %Activity{} = like_activity <- get_existing_like(actor.ap_id, object),
   #        unlike_data <- make_unlike_data(actor, like_activity, activity_id),
-  #        {:ok, unlike_activity, _object} <- insert(unlike_data, local),
+  #        {:ok, unlike_activity} <- create_activity(unlike_data, local),
+  #        {:ok, _object} <- insert_full_object(data),
   #        {:ok, _activity} <- Repo.delete(like_activity),
   #        {:ok, object} <- remove_like_from_object(like_activity, object),
   #        :ok <- maybe_federate(unlike_activity) do
@@ -247,7 +245,8 @@ defmodule Mobilizon.Service.ActivityPub do
       ) do
     with true <- is_public?(object),
          announce_data <- make_announce_data(actor, object, activity_id, public),
-         {:ok, activity, object} <- insert(announce_data, local),
+         {:ok, activity} <- create_activity(announce_data, local),
+         {:ok, object} <- insert_full_object(announce_data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     else
@@ -265,7 +264,8 @@ defmodule Mobilizon.Service.ActivityPub do
       ) do
     with announce_activity <- make_announce_data(actor, object, cancelled_activity_id),
          unannounce_data <- make_unannounce_data(actor, announce_activity, activity_id),
-         {:ok, unannounce_activity, _object} <- insert(unannounce_data, local),
+         {:ok, unannounce_activity} <- create_activity(unannounce_data, local),
+         {:ok, object} <- insert_full_object(unannounce_data),
          :ok <- maybe_federate(unannounce_activity) do
       {:ok, unannounce_activity, object}
     else
@@ -282,7 +282,8 @@ defmodule Mobilizon.Service.ActivityPub do
          activity_follow_id <-
            activity_id || follow_url,
          data <- make_follow_data(followed, follower, activity_follow_id),
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- insert_full_object(data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     else
@@ -304,12 +305,14 @@ defmodule Mobilizon.Service.ActivityPub do
              follower,
              "#{MobilizonWeb.Endpoint.url()}/follow/#{follow_id}/activity"
            ),
-         {:ok, follow_activity, _object} <- insert(data, local),
+         {:ok, follow_activity} <- create_activity(data, local),
+         {:ok, _object} <- insert_full_object(data),
          activity_unfollow_id <-
            activity_id || "#{MobilizonWeb.Endpoint.url()}/unfollow/#{follow_id}/activity",
          unfollow_data <-
            make_unfollow_data(follower, followed, follow_activity, activity_unfollow_id),
-         {:ok, activity, object} <- insert(unfollow_data, local),
+         {:ok, activity} <- create_activity(unfollow_data, local),
+         {:ok, object} <- insert_full_object(unfollow_data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     else
@@ -331,7 +334,8 @@ defmodule Mobilizon.Service.ActivityPub do
     }
 
     with {:ok, _} <- Events.delete_event(event),
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- insert_full_object(data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     end
@@ -347,7 +351,8 @@ defmodule Mobilizon.Service.ActivityPub do
     }
 
     with {:ok, _} <- Events.delete_comment(comment),
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- insert_full_object(data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     end
@@ -363,7 +368,8 @@ defmodule Mobilizon.Service.ActivityPub do
     }
 
     with {:ok, _} <- Actors.delete_actor(actor),
-         {:ok, activity, object} <- insert(data, local),
+         {:ok, activity} <- create_activity(data, local),
+         {:ok, object} <- insert_full_object(data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, object}
     end
@@ -384,9 +390,10 @@ defmodule Mobilizon.Service.ActivityPub do
       end
 
     with flag_data <- make_flag_data(params, additional),
-         {:ok, activity, report} <- insert(flag_data, local),
+         {:ok, activity} <- create_activity(flag_data, local),
+         {:ok, object} <- insert_full_object(flag_data),
          :ok <- maybe_federate(activity) do
-      {:ok, activity, report}
+      {:ok, activity, object}
     end
   end
 
@@ -403,7 +410,8 @@ defmodule Mobilizon.Service.ActivityPub do
          join_data <- Convertible.model_to_as(participant),
          join_data <- Map.put(join_data, "to", [event.organizer_actor.url]),
          join_data <- Map.put(join_data, "cc", []),
-         {:ok, activity, _} <- insert(join_data, local),
+         {:ok, activity} <- create_activity(join_data, local),
+         {:ok, _object} <- insert_full_object(join_data),
          :ok <- maybe_federate(activity) do
       if role === :participant do
         accept(
@@ -443,7 +451,8 @@ defmodule Mobilizon.Service.ActivityPub do
            "to" => [event.organizer_actor.url],
            "cc" => []
          },
-         {:ok, activity, _} <- insert(leave_data, local),
+         {:ok, activity} <- create_activity(leave_data, local),
+         {:ok, _object} <- insert_full_object(leave_data),
          :ok <- maybe_federate(activity) do
       {:ok, activity, participant}
     end
diff --git a/lib/service/activity_pub/converters/actor.ex b/lib/service/activity_pub/converters/actor.ex
index 853173867..5e8c4ac3c 100644
--- a/lib/service/activity_pub/converters/actor.ex
+++ b/lib/service/activity_pub/converters/actor.ex
@@ -15,12 +15,30 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Actor do
   @impl Converter
   @spec as_to_model_data(map()) :: map()
   def as_to_model_data(object) do
+    avatar =
+      object["icon"]["url"] &&
+        %{
+          "name" => object["icon"]["name"] || "avatar",
+          "url" => object["icon"]["url"]
+        }
+
+    banner =
+      object["image"]["url"] &&
+        %{
+          "name" => object["image"]["name"] || "banner",
+          "url" => object["image"]["url"]
+        }
+
     %{
       "type" => String.to_existing_atom(object["type"]),
-      "preferred_username" => object["preferred_username"],
+      "preferred_username" => object["preferredUsername"],
       "summary" => object["summary"],
       "url" => object["url"],
-      "name" => object["name"]
+      "name" => object["name"],
+      "avatar" => avatar,
+      "banner" => banner,
+      "keys" => object["publicKey"]["publicKeyPem"],
+      "manually_approves_followers" => object["manuallyApprovesFollowers"]
     }
   end
 
diff --git a/lib/service/activity_pub/converters/event.ex b/lib/service/activity_pub/converters/event.ex
index 0bfabd6aa..00f8cf0eb 100644
--- a/lib/service/activity_pub/converters/event.ex
+++ b/lib/service/activity_pub/converters/event.ex
@@ -57,6 +57,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
         "organizer_actor_id" => actor_id,
         "picture_id" => picture_id,
         "begins_on" => object["startTime"],
+        "ends_on" => object["endTime"],
         "category" => object["category"],
         "url" => object["id"],
         "uuid" => object["uuid"],
@@ -173,7 +174,8 @@ defmodule Mobilizon.Service.ActivityPub.Converters.Event do
       "startTime" => event.begins_on |> date_to_string(),
       "endTime" => event.ends_on |> date_to_string(),
       "tag" => event.tags |> build_tags(),
-      "id" => event.url
+      "id" => event.url,
+      "url" => event.url
     }
 
     res =
diff --git a/lib/service/activity_pub/transmogrifier.ex b/lib/service/activity_pub/transmogrifier.ex
index a29b61ed5..2d5de4370 100644
--- a/lib/service/activity_pub/transmogrifier.ex
+++ b/lib/service/activity_pub/transmogrifier.ex
@@ -295,19 +295,15 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
         %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => _actor_id} =
           data
       )
-      when object_type in ["Person", "Application", "Service", "Organization"] do
+      when object_type in ["Person", "Group", "Application", "Service", "Organization"] do
     case Actors.get_actor_by_url(object["id"]) do
-      {:ok, %Actor{url: url}} ->
-        {:ok, new_actor_data} = ActivityPub.actor_data_from_actor_object(object)
-
-        Actors.insert_or_update_actor(new_actor_data)
-
+      {:ok, %Actor{url: actor_url}} ->
         ActivityPub.update(%{
           local: false,
           to: data["to"] || [],
           cc: data["cc"] || [],
           object: object,
-          actor: url
+          actor: actor_url
         })
 
       e ->
@@ -316,6 +312,28 @@ defmodule Mobilizon.Service.ActivityPub.Transmogrifier do
     end
   end
 
+  def handle_incoming(
+        %{"type" => "Update", "object" => %{"type" => "Event"} = object, "actor" => actor} =
+          _update
+      ) do
+    with {:ok, %{"actor" => existing_organizer_actor_url} = _existing_event_data} <-
+           fetch_obj_helper_as_activity_streams(object),
+         {:ok, %Actor{url: actor_url}} <- actor |> Utils.get_url() |> Actors.get_actor_by_url(),
+         true <- Utils.get_url(existing_organizer_actor_url) == actor_url do
+      ActivityPub.update(%{
+        local: false,
+        to: object["to"] || [],
+        cc: object["cc"] || [],
+        object: object,
+        actor: actor_url
+      })
+    else
+      e ->
+        Logger.debug(inspect(e))
+        :error
+    end
+  end
+
   def handle_incoming(
         %{
           "type" => "Undo",
diff --git a/lib/service/activity_pub/utils.ex b/lib/service/activity_pub/utils.ex
index 31631c062..d68c62287 100644
--- a/lib/service/activity_pub/utils.ex
+++ b/lib/service/activity_pub/utils.ex
@@ -29,6 +29,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
   alias MobilizonWeb.Router.Helpers, as: Routes
   alias MobilizonWeb.Endpoint
 
+  @actor_types ["Group", "Person", "Application"]
+
   # Some implementations send the actor URI as the actor field, others send the entire actor object,
   # so figure out what the actor's URI is based on what we have.
   def get_url(%{"id" => id}), do: id
@@ -119,7 +121,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
   @doc """
   Inserts a full object if it is contained in an activity.
   """
-  def insert_full_object(%{"object" => %{"type" => "Event"} = object_data})
+  def insert_full_object(%{"object" => %{"type" => "Event"} = object_data, "type" => "Create"})
       when is_map(object_data) do
     with {:ok, object_data} <-
            Converters.Event.as_to_model_data(object_data),
@@ -128,7 +130,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
     end
   end
 
-  def insert_full_object(%{"object" => %{"type" => "Group"} = object_data})
+  def insert_full_object(%{"object" => %{"type" => "Group"} = object_data, "type" => "Create"})
       when is_map(object_data) do
     with object_data <-
            Map.put(object_data, "preferred_username", object_data["preferredUsername"]),
@@ -140,7 +142,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
   @doc """
   Inserts a full object if it is contained in an activity.
   """
-  def insert_full_object(%{"object" => %{"type" => "Note"} = object_data})
+  def insert_full_object(%{"object" => %{"type" => "Note"} = object_data, "type" => "Create"})
       when is_map(object_data) do
     with data <- Converters.Comment.as_to_model_data(object_data),
          {:ok, %Comment{} = comment} <- Events.create_comment(data) do
@@ -177,6 +179,39 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
 
   def insert_full_object(_), do: {:ok, nil}
 
+  @doc """
+  Update an object
+  """
+  @spec update_object(struct(), map()) :: {:ok, struct()} | any()
+  def update_object(object, object_data)
+
+  def update_object(event_url, %{
+        "object" => %{"type" => "Event"} = object_data,
+        "type" => "Update"
+      })
+      when is_map(object_data) do
+    with {:event_not_found, %Event{} = event} <-
+           {:event_not_found, Events.get_event_by_url(event_url)},
+         {:ok, object_data} <- Converters.Event.as_to_model_data(object_data),
+         {:ok, %Event{} = event} <- Events.update_event(event, object_data) do
+      {:ok, event}
+    end
+  end
+
+  def update_object(actor_url, %{
+        "object" => %{"type" => type_actor} = object_data,
+        "type" => "Update"
+      })
+      when is_map(object_data) and type_actor in @actor_types do
+    with {:ok, %Actor{} = actor} <- Actors.get_actor_by_url(actor_url),
+         object_data <- Converters.Actor.as_to_model_data(object_data),
+         {:ok, %Actor{} = actor} <- Actors.update_actor(actor, object_data) do
+      {:ok, actor}
+    end
+  end
+
+  def update_object(_, _), do: {:ok, nil}
+
   #### Like-related helpers
 
   #  @doc """
@@ -264,7 +299,8 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
           String.t(),
           map(),
           list(),
-          map()
+          map(),
+          String.t()
         ) :: map()
   def make_event_data(
         actor,
@@ -273,10 +309,12 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
         content_html,
         picture \\ nil,
         tags \\ [],
-        metadata \\ %{}
+        metadata \\ %{},
+        uuid \\ nil,
+        url \\ nil
       ) do
     Logger.debug("Making event data")
-    uuid = Ecto.UUID.generate()
+    uuid = uuid || Ecto.UUID.generate()
 
     res = %{
       "type" => "Event",
@@ -285,9 +323,10 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
       "content" => content_html,
       "name" => title,
       "startTime" => metadata.begins_on,
+      "endTime" => metadata.ends_on,
       "category" => metadata.category,
       "actor" => actor,
-      "id" => Routes.page_url(Endpoint, :event, uuid),
+      "id" => url || Routes.page_url(Endpoint, :event, uuid),
       "uuid" => uuid,
       "tag" =>
         tags |> Enum.uniq() |> Enum.map(fn tag -> %{"type" => "Hashtag", "name" => "##{tag}"} end)
@@ -505,7 +544,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
         activity_id,
         public
       )
-      when type in ["Group", "Person", "Application"] do
+      when type in @actor_types do
     do_make_announce_data(actor_url, actor_followers_url, url, url, activity_id, public)
   end
 
diff --git a/schema.graphql b/schema.graphql
index 8a8432422..a3ac11790 100644
--- a/schema.graphql
+++ b/schema.graphql
@@ -1,5 +1,5 @@
 # source: http://localhost:4000/api
-# timestamp: Mon Sep 02 2019 16:41:17 GMT+0200 (GMT+02:00)
+# timestamp: Thu Sep 05 2019 13:00:10 GMT+0200 (GMT+02:00)
 
 schema {
   query: RootQueryType
@@ -440,7 +440,7 @@ enum EventVisibility {
   """Visible only to people members of the group or followers of the person"""
   PRIVATE
 
-  """Publically listed and federated. Can be shared."""
+  """Publicly listed and federated. Can be shared."""
   PUBLIC
 
   """Visible only to people with the link - or invited"""
@@ -823,7 +823,7 @@ type RootMutationType {
   """Create an event"""
   createEvent(
     beginsOn: DateTime!
-    category: String
+    category: String = "meeting"
     description: String!
     endsOn: DateTime
     onlineAddress: String
@@ -852,11 +852,6 @@ type RootMutationType {
 
   """Create a group"""
   createGroup(
-    """
-    The actor's username which will be the admin (otherwise user's default one)
-    """
-    adminActorUsername: String
-
     """
     The avatar for the group, either as an object or directly the ID of an existing Picture
     """
@@ -867,14 +862,17 @@ type RootMutationType {
     """
     banner: PictureInput
 
-    """The summary for the group"""
-    description: String = ""
+    """The identity that creates the group"""
+    creatorActorId: Int!
 
     """The displayed name for the group"""
     name: String
 
     """The name for the group"""
     preferredUsername: String!
+
+    """The summary for the group"""
+    summary: String = ""
   ): Group
 
   """Create a new person for user"""
@@ -969,6 +967,34 @@ type RootMutationType {
   """Send a link through email to reset user password"""
   sendResetPassword(email: String!, locale: String = "en"): String
 
+  """Update an event"""
+  updateEvent(
+    beginsOn: DateTime
+    category: String
+    description: String
+    endsOn: DateTime
+    eventId: ID!
+    onlineAddress: String
+    options: EventOptionsInput
+    organizerActorId: ID
+    phoneAddress: String
+    physicalAddress: AddressInput
+
+    """
+    The picture for the event, either as an object or directly the ID of an existing Picture
+    """
+    picture: PictureInput
+    public: Boolean
+    publishAt: DateTime
+    state: Int
+    status: Int
+
+    """The list of tags associated to the event"""
+    tags: [String]
+    title: String
+    visibility: EventVisibility
+  ): Event
+
   """Update an identity"""
   updatePerson(
     """
diff --git a/test/fixtures/mastodon-update.json b/test/fixtures/mastodon-update.json
index ab573c82a..c7d697a72 100644
--- a/test/fixtures/mastodon-update.json
+++ b/test/fixtures/mastodon-update.json
@@ -1,24 +1,24 @@
 {
   "type": "Update",
   "object": {
-    "url": "http://mastodon.example.org/@gargron",
+    "url": "https://framapiaf.org/@framasoft",
     "type": "Person",
     "summary": "<p>Some bio</p>",
     "publicKey": {
       "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0gs3VnQf6am3R+CeBV4H\nlfI1HZTNRIBHgvFszRZkCERbRgEWMu+P+I6/7GJC5H5jhVQ60z4MmXcyHOGmYMK/\n5XyuHQz7V2Ssu1AxLfRN5Biq1ayb0+DT/E7QxNXDJPqSTnstZ6C7zKH/uAETqg3l\nBonjCQWyds+IYbQYxf5Sp3yhvQ80lMwHML3DaNCMlXWLoOnrOX5/yK5+dedesg2\n/HIvGk+HEt36vm6hoH7bwPuEkgA++ACqwjXRe5Mta7i3eilHxFaF8XIrJFARV0t\nqOu4GID/jG6oA+swIWndGrtR2QRJIt9QIBFfK3HG5M0koZbY1eTqwNFRHFL3xaD\nUQIDAQAB\n-----END PUBLIC KEY-----\n",
-      "owner": "http://mastodon.example.org/users/gargron",
-      "id": "http://mastodon.example.org/users/gargron#main-key"
+      "owner": "https://framapiaf.org/users/framasoft",
+      "id": "https://framapiaf.org/users/framasoft#main-key"
     },
-    "preferredUsername": "gargron",
-    "outbox": "http://mastodon.example.org/users/gargron/outbox",
-    "name": "gargle",
+    "preferredUsername": "framasoft",
+    "outbox": "https://framapiaf.org/users/framasoft/outbox",
+    "name": "nextsoft",
     "manuallyApprovesFollowers": false,
-    "inbox": "http://mastodon.example.org/users/gargron/inbox",
-    "id": "http://mastodon.example.org/users/gargron",
-    "following": "http://mastodon.example.org/users/gargron/following",
-    "followers": "http://mastodon.example.org/users/gargron/followers",
+    "inbox": "https://framapiaf.org/users/framasoft/inbox",
+    "id": "https://framapiaf.org/users/framasoft",
+    "following": "https://framapiaf.org/users/framasoft/following",
+    "followers": "https://framapiaf.org/users/framasoft/followers",
     "endpoints": {
-      "sharedInbox": "http://mastodon.example.org/inbox"
+      "sharedInbox": "https://framapiaf.org/inbox"
     },
     "icon":{
       "type":"Image",
@@ -31,8 +31,8 @@
       "url":"https://files.mastodon.social/accounts/headers/000/000/001/original/c91b871f294ea63e.png"
     }
   },
-  "id": "http://mastodon.example.org/users/gargron#updates/1519563538",
-  "actor": "http://mastodon.example.org/users/gargron",
+  "id": "https://framapiaf.org/users/gargron#updates/1519563538",
+  "actor": "https://framapiaf.org/users/framasoft",
   "@context": [
     "https://www.w3.org/ns/activitystreams",
     "https://w3id.org/security/v1",
diff --git a/test/mobilizon/actors/actors_test.exs b/test/mobilizon/actors/actors_test.exs
index bcb320724..6b2ae6917 100644
--- a/test/mobilizon/actors/actors_test.exs
+++ b/test/mobilizon/actors/actors_test.exs
@@ -270,10 +270,8 @@ defmodule Mobilizon.ActorsTest do
       assert %Actor{} = actor
       assert actor.summary == "some updated description"
       assert actor.name == "some updated name"
-      assert actor.domain == "some updated domain"
       assert actor.keys == "some updated keys"
       refute actor.suspended
-      assert actor.preferred_username == "some updated username"
     end
 
     test "update_actor/2 with valid data updates the actor and it's media files", %{
@@ -310,10 +308,8 @@ defmodule Mobilizon.ActorsTest do
       assert %Actor{} = actor
       assert actor.summary == "some updated description"
       assert actor.name == "some updated name"
-      assert actor.domain == "some updated domain"
       assert actor.keys == "some updated keys"
       refute actor.suspended
-      assert actor.preferred_username == "some updated username"
 
       refute File.exists?(
                Mobilizon.CommonConfig.get!([MobilizonWeb.Uploaders.Local, :uploads]) <>
diff --git a/test/mobilizon/service/activity_pub/activity_pub_test.exs b/test/mobilizon/service/activity_pub/activity_pub_test.exs
index 638ed0a18..c744d3d68 100644
--- a/test/mobilizon/service/activity_pub/activity_pub_test.exs
+++ b/test/mobilizon/service/activity_pub/activity_pub_test.exs
@@ -9,6 +9,7 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
   import Mobilizon.Factory
 
   alias Mobilizon.Events
+  alias Mobilizon.Events.Event
   alias Mobilizon.Actors.Actor
   alias Mobilizon.Actors
   alias Mobilizon.Service.HTTPSignatures.Signature
@@ -137,11 +138,14 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
   end
 
   describe "update" do
+    @updated_actor_summary "This is an updated actor"
+
     test "it creates an update activity with the new actor data" do
       actor = insert(:actor)
       actor_data = MobilizonWeb.ActivityPub.ActorView.render("actor.json", %{actor: actor})
+      actor_data = Map.put(actor_data, "summary", @updated_actor_summary)
 
-      {:ok, update, _} =
+      {:ok, update, updated_actor} =
         ActivityPub.update(%{
           actor: actor_data["url"],
           to: [actor.url <> "/followers"],
@@ -153,6 +157,42 @@ defmodule Mobilizon.Service.ActivityPub.ActivityPubTest do
       assert update.data["to"] == [actor.url <> "/followers"]
       assert update.data["object"]["id"] == actor_data["id"]
       assert update.data["object"]["type"] == actor_data["type"]
+      assert update.data["object"]["summary"] == @updated_actor_summary
+
+      refute updated_actor.summary == actor.summary
+
+      {:ok, %Actor{} = database_actor} = Mobilizon.Actors.get_actor_by_url(actor.url)
+      assert database_actor.summary == @updated_actor_summary
+      assert database_actor.preferred_username == actor.preferred_username
+    end
+
+    @updated_start_time DateTime.utc_now() |> DateTime.truncate(:second)
+
+    test "it creates an update activity with the new event data" do
+      actor = insert(:actor)
+      event = insert(:event, organizer_actor: actor)
+      event_data = Mobilizon.Service.ActivityPub.Converters.Event.model_to_as(event)
+      event_data = Map.put(event_data, "startTime", @updated_start_time)
+
+      {:ok, update, updated_event} =
+        ActivityPub.update(%{
+          actor: actor.url,
+          to: [actor.url <> "/followers"],
+          cc: [],
+          object: event_data
+        })
+
+      assert update.data["actor"] == actor.url
+      assert update.data["to"] == [actor.url <> "/followers"]
+      assert update.data["object"]["id"] == event_data["id"]
+      assert update.data["object"]["type"] == event_data["type"]
+      assert update.data["object"]["startTime"] == @updated_start_time
+
+      refute updated_event.begins_on == event.begins_on
+
+      %Event{} = database_event = Mobilizon.Events.get_event_by_url(event.url)
+      assert database_event.begins_on == @updated_start_time
+      assert database_event.title == event.title
     end
   end
 end
diff --git a/test/mobilizon/service/activity_pub/converters/actor_test.exs b/test/mobilizon/service/activity_pub/converters/actor_test.exs
index e0b9fefb7..242fc9385 100644
--- a/test/mobilizon/service/activity_pub/converters/actor_test.exs
+++ b/test/mobilizon/service/activity_pub/converters/actor_test.exs
@@ -17,7 +17,7 @@ defmodule Mobilizon.Service.ActivityPub.Converters.ActorTest do
       actor =
         ActorConverter.as_to_model_data(%{
           "type" => "Person",
-          "preferred_username" => "test_account"
+          "preferredUsername" => "test_account"
         })
 
       assert actor["type"] == :Person
diff --git a/test/mobilizon/service/activity_pub/transmogrifier_test.exs b/test/mobilizon/service/activity_pub/transmogrifier_test.exs
index dde9e57b5..cd636a3d9 100644
--- a/test/mobilizon/service/activity_pub/transmogrifier_test.exs
+++ b/test/mobilizon/service/activity_pub/transmogrifier_test.exs
@@ -330,7 +330,7 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
       assert data["object"] == comment.url
     end
 
-    test "it works for incoming update activities" do
+    test "it works for incoming update activities on actors" do
       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
 
       {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
@@ -349,11 +349,37 @@ defmodule Mobilizon.Service.ActivityPub.TransmogrifierTest do
       {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data)
 
       {:ok, %Actor{} = actor} = Actors.get_actor_by_url(data["actor"])
-      assert actor.name == "gargle"
+      assert actor.name == "nextsoft"
 
       assert actor.summary == "<p>Some bio</p>"
     end
 
+    test "it works for incoming update activities on events" do
+      data = File.read!("test/fixtures/mobilizon-post-activity.json") |> Jason.decode!()
+
+      {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(data)
+      update_data = File.read!("test/fixtures/mastodon-update.json") |> Jason.decode!()
+
+      object =
+        data["object"]
+        |> Map.put("actor", data["actor"])
+        |> Map.put("name", "My updated event")
+        |> Map.put("id", data["object"]["id"])
+        |> Map.put("type", "Event")
+
+      update_data =
+        update_data
+        |> Map.put("actor", data["actor"])
+        |> Map.put("object", object)
+
+      {:ok, %Activity{data: data, local: false}, _} = Transmogrifier.handle_incoming(update_data)
+
+      %Event{} = event = Events.get_event_by_url(data["object"]["id"])
+      assert event.title == "My updated event"
+
+      assert event.description == data["object"]["content"]
+    end
+
     #     test "it works for incoming update activities which lock the account" do
     #       data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
 
diff --git a/test/mobilizon_web/resolvers/event_resolver_test.exs b/test/mobilizon_web/resolvers/event_resolver_test.exs
index 077f2ec05..c01f9d0fd 100644
--- a/test/mobilizon_web/resolvers/event_resolver_test.exs
+++ b/test/mobilizon_web/resolvers/event_resolver_test.exs
@@ -58,6 +58,40 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
                json_response(res, 200)["errors"]
     end
 
+    test "create_event/3 should check the organizer_actor_id is owned by the user", %{
+      conn: conn,
+      user: user
+    } do
+      another_actor = insert(:actor)
+
+      begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
+
+      mutation = """
+          mutation {
+              createEvent(
+                  title: "come to my event",
+                  description: "it will be fine",
+                  begins_on: "#{begins_on}",
+                  organizer_actor_id: "#{another_actor.id}",
+                  category: "birthday"
+              ) {
+                title,
+                uuid
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["data"]["createEvent"] == nil
+
+      assert hd(json_response(res, 200)["errors"])["message"] ==
+               "Organizer actor id is not owned by the user"
+    end
+
     test "create_event/3 creates an event", %{conn: conn, actor: actor, user: user} do
       mutation = """
           mutation {
@@ -384,6 +418,166 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
       assert json_response(res, 200)["data"]["createEvent"]["picture"]["url"]
     end
 
+    test "update_event/3 should check the event exists", %{conn: conn, actor: _actor, user: user} do
+      mutation = """
+          mutation {
+              updateEvent(
+                  event_id: 45,
+                  title: "my event updated"
+              ) {
+                title,
+                uuid,
+                tags {
+                  title,
+                  slug
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] == "Event not found"
+    end
+
+    test "update_event/3 should check the user is an administrator", %{
+      conn: conn,
+      actor: _actor,
+      user: user
+    } do
+      event = insert(:event)
+
+      mutation = """
+          mutation {
+              updateEvent(
+                  title: "my event updated",
+                  event_id: #{event.id}
+              ) {
+                title,
+                uuid,
+                tags {
+                  title,
+                  slug
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert hd(json_response(res, 200)["errors"])["message"] == "User doesn't own actor"
+    end
+
+    test "update_event/3 updates an event", %{conn: conn, actor: actor, user: user} do
+      event = insert(:event, organizer_actor: actor)
+
+      begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
+
+      mutation = """
+          mutation {
+              updateEvent(
+                  title: "my event updated",
+                  description: "description updated",
+                  begins_on: "#{begins_on}",
+                  event_id: #{event.id},
+                  organizer_actor_id: "#{actor.id}",
+                  category: "birthday",
+                  tags: ["tag1_updated", "tag2_updated"]
+              ) {
+                title,
+                uuid,
+                url,
+                tags {
+                  title,
+                  slug
+                }
+              }
+            }
+      """
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> post("/api", AbsintheHelpers.mutation_skeleton(mutation))
+
+      assert json_response(res, 200)["errors"] == nil
+      assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated"
+      assert json_response(res, 200)["data"]["updateEvent"]["uuid"] == event.uuid
+      assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url
+
+      assert json_response(res, 200)["data"]["updateEvent"]["tags"] == [
+               %{"slug" => "tag1-updated", "title" => "tag1_updated"},
+               %{"slug" => "tag2-updated", "title" => "tag2_updated"}
+             ]
+    end
+
+    test "update_event/3 updates an event with a new picture", %{
+      conn: conn,
+      actor: actor,
+      user: user
+    } do
+      event = insert(:event, organizer_actor: actor)
+
+      begins_on = DateTime.utc_now() |> DateTime.truncate(:second) |> DateTime.to_iso8601()
+
+      mutation = """
+          mutation {
+              updateEvent(
+                  title: "my event updated",
+                  description: "description updated",
+                  begins_on: "#{begins_on}",
+                  event_id: #{event.id},
+                  organizer_actor_id: "#{actor.id}",
+                  category: "birthday",
+                  picture: {
+                    picture: {
+                      name: "picture for my event",
+                      alt: "A very sunny landscape",
+                      file: "event.jpg",
+                      actor_id: "#{actor.id}"
+                    }
+                  }
+              ) {
+                title,
+                uuid,
+                url,
+                picture {
+                  name,
+                  url
+                }
+              }
+          }
+      """
+
+      map = %{
+        "query" => mutation,
+        "event.jpg" => %Plug.Upload{
+          path: "test/fixtures/picture.png",
+          filename: "event.jpg"
+        }
+      }
+
+      res =
+        conn
+        |> auth_conn(user)
+        |> put_req_header("content-type", "multipart/form-data")
+        |> post("/api", map)
+
+      assert json_response(res, 200)["errors"] == nil
+      assert json_response(res, 200)["data"]["updateEvent"]["title"] == "my event updated"
+      assert json_response(res, 200)["data"]["updateEvent"]["uuid"] == event.uuid
+      assert json_response(res, 200)["data"]["updateEvent"]["url"] == event.url
+
+      assert json_response(res, 200)["data"]["updateEvent"]["picture"]["name"] ==
+               "picture for my event"
+    end
+
     test "list_events/3 returns events", context do
       event = insert(:event)
 
diff --git a/test/support/factory.ex b/test/support/factory.ex
index eee37f91f..e91cd42a5 100644
--- a/test/support/factory.ex
+++ b/test/support/factory.ex
@@ -67,8 +67,8 @@ defmodule Mobilizon.Factory do
 
   def tag_factory do
     %Mobilizon.Events.Tag{
-      title: "MyTag",
-      slug: sequence("MyTag")
+      title: sequence("MyTag"),
+      slug: sequence("my-tag")
     }
   end