diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e93c90f9de4deefb1dd0fc6ed8a71b774e888d60..3dfe9d5240096ffba1775d88d5fbcfbfde7cb791 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -2,7 +2,7 @@ image: docker:19.03.12
 
 variables:
   DOCKER_TLS_CERTDIR: "/certs"
-  IMAGE_VER: 1.12.0
+  IMAGE_VER: 2.0.0
 
 services:
   - docker:19.03.12-dind
diff --git a/lib/CF/Controller/Events.pm b/lib/CF/Controller/Events.pm
new file mode 100644
index 0000000000000000000000000000000000000000..cdbc1baff953850e134d85477179665693ad4bde
--- /dev/null
+++ b/lib/CF/Controller/Events.pm
@@ -0,0 +1,124 @@
+package CF::Controller::Events;
+
+use feature 'signatures';
+no warnings qw{ experimental::signatures };
+use Mojo::Base 'Mojolicious::Controller';
+use UUID::URandom qw(create_uuid_string);
+
+sub create ($c) {
+    $c->openapi->valid_input or return;
+
+    my $args = $c->req->json;
+
+    my $event = $c->schema->resultset('Event')->create({
+        uuid        => create_uuid_string(),
+        type        => $args->{type},
+        owner_id    => $c->user->{id},
+        start       => $args->{start},
+        finish      => $args->{finish},
+        name        => $args->{name},
+        description => $args->{description},
+        organizer   => $args->{organizer} || $c->user->{name},
+    });
+
+    $c->render(
+        status  => 201,
+        openapi => { id => $event->id },
+    );
+};
+
+sub get ($c) {
+    $c->openapi->valid_input or return;
+
+    my $event = $c->schema->resultset('Event')->find($c->stash->{id});
+    return $c->error(404, 'Event not found') if ! $event;
+
+    my $formatted = $event->format();
+    $formatted->{acl} = [];
+    $formatted->{stream_url} = $event->stream_url
+                             || '/hls/' . $event->uuid . '.m3u8';
+
+    my $roles = $event->user_roles( $c->user );
+
+    if ( $roles->{chairman} ) {
+        $formatted->{meet_url}   = $c->cfg->{jitsi_base_url}  . $event->uuid;
+        $formatted->{record_url} = $c->cfg->{codimd_base_url} . $event->uuid;
+    }
+    elsif ( ! $event->is_published ) {
+        # neni zverejnen
+        return $c->error(404, 'Event not found');
+    }
+
+    $c->render(openapi => $c->spec_filter($formatted, 'Event'));
+}
+
+sub list ($c) {
+    $c->openapi->valid_input or return;
+    my $args = $c->validation->output;
+
+    my ($cond, $attrs) = $c->search_parametrs( $args );
+
+    $cond->{deleted} = undef;
+
+    $cond->{type} = $args->{type} if $args->{type};
+
+    if ( ! $args->{show_finished} ) {
+        $cond->{state} = [0, 1];
+    }
+
+    if ( ! $c->user_roles->{organizer} ) {
+        $cond->{is_published} = 't';
+    }
+
+    my @events = ();
+    my $count = $c->schema->resultset('Event')->count($cond);
+
+    if ( $count ) {
+        my $events = $c->schema->resultset('Event')->search($cond, $attrs);
+
+        EVENT:
+        while ( my $event = $events->next() ) {
+            my $formatted = $event->format();
+            push @events, $c->spec_filter($formatted, 'EventInList');
+        }
+    }
+
+    $c->render(json => {
+        data  => \@events,
+        total => $count,
+    });
+}
+
+sub update ($c) {
+    $c->openapi->valid_input or return;
+
+    my $args = $c->req->json;
+
+    my $event = $c->schema->resultset('Event')->find($c->stash->{id});
+    return $c->error(404, 'Event not found') if ! $event;
+
+    my $update = $c->prepare_update_data( $event, $c->req->json );
+    my $guard  = $c->schema->txn_scope_guard;
+
+    $event->update({
+        %{ $update },
+        changed => \'now()',
+    });
+
+    $guard->commit;
+    $c->render(status => 204, text => '');
+}
+
+sub delete ($c) {
+    $c->openapi->valid_input or return;
+
+    my $event = $c->schema->resultset('Event')->find($c->stash->{id});
+    return $c->error(404, 'Event not found') if ! $event;
+
+    my $guard  = $c->schema->txn_scope_guard;
+    $event->update({ deleted => \'now()', });
+
+    $guard->commit;
+    $c->render(status => 204, text => '');
+}
+1;
diff --git a/lib/CF/Schema/Result/Event.pm b/lib/CF/Schema/Result/Event.pm
index 3c2799dd7e6208d32165386d6297eadc12ef3a3b..01e73357235e1c8b55e6241e004b88c0d097787b 100644
--- a/lib/CF/Schema/Result/Event.pm
+++ b/lib/CF/Schema/Result/Event.pm
@@ -2,6 +2,8 @@ package CF::Schema::Result::Event;
 
 use strict;
 use warnings;
+use feature 'signatures';
+no warnings qw{ experimental::signatures };
 
 use base 'DBIx::Class::Core';
 
@@ -19,9 +21,12 @@ __PACKAGE__->add_columns(
     qw(
         uuid
         type
+        state
         owner_id
+        is_published
         start
         finish
+        deleted
         name
         description
         organizer
@@ -43,4 +48,37 @@ __PACKAGE__->has_many(
 
 __PACKAGE__->set_primary_key('id');
 
+sub format ($self) {
+
+    my $event = {
+        id              => $self->id,
+        type            => $self->type,
+        state           => $self->state,
+        is_published    => $self->is_published ? \1 : \0,
+        start           => $self->start,
+        finish          => $self->finish,
+        name            => $self->name,
+        description     => $self->description,
+        organizer       => $self->organizer,
+        stream_url      => $self->stream_url,
+    };
+
+### ACL???
+
+    return $event;
+}
+
+sub user_roles ($self, $user) {
+
+    return undef if ! $user;
+
+    my $roles = {};
+
+    if ( $self->owner_id == $user->{id} ) {
+        $roles->{chairman}  = 1;
+    }
+
+    return $roles;
+}
+
 1;
diff --git a/openapi.yaml b/openapi.yaml
index c7294af660bf7a59f61d668ae4d8e99afcef8531..a3932930e04d969008d4f6d9cc1ff260ea8d7b3d 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -1,7 +1,7 @@
 openapi: 3.0.3
 
 info:
-  version: 1.12
+  version: 2.0
   title: CF Online
   description: CF Online
   license:
@@ -83,12 +83,14 @@ components:
         id:
           type: integer
           readOnly: true
-        uuid:
-          type: string
-          readOnly: true
         type:
           type: integer
           enum: [1, 2, 3]
+        state:
+          type: integer
+          enum: [0, 1, 2]
+        is_published:
+          type: boolean
         start:
           type: string
           maxLength: 20
@@ -104,16 +106,53 @@ components:
           type: string
         stream_url:
           type: string
+          nullable: true
         meet_url:
           type: string
           readOnly: true
+          nullable: true
         record_url:
           type: string
           readOnly: true
+          nullable: true
         acl:
            type: array
            items:
               $ref: '#/components/schemas/EventRole'
+    EventInList:
+      type: object
+      description: Udalost
+      properties:
+        id:
+          type: integer
+          readOnly: true
+        type:
+          type: integer
+          enum: [1, 2, 3]
+        state:
+          type: integer
+          enum: [0, 1, 2]
+        is_published:
+          type: boolean
+        start:
+          type: string
+          maxLength: 20
+        finish:
+          type: string
+          maxLength: 20
+          nullable: true
+        name:
+          type: string
+        description:
+          type: string
+        organizer:
+          type: string
+        stream_url:
+          type: string
+          nullable: true
+        banner:
+          type: string
+          nullable: true
     EventRole:
       type: object
       properties:
@@ -276,6 +315,185 @@ paths:
         204:
           description: Program entry updated
 
+  /events:
+    post:
+      x-mojo-to: events#create
+      security:
+        - Bearer: ['organizer']
+      tags:
+        - events
+      summary: "Pridat udalost"
+      operationId: createEvent
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                type:
+                  type: integer
+                  enum: [1, 2, 3]
+                  example: 1
+                start:
+                  type: string
+                  maxLength: 20
+                  example: '18.07.2021 10:00'
+                finish:
+                  type: string
+                  maxLength: 20
+                  example: '18.07.2021 18:00'
+                name:
+                  type: string
+                  maxLength: 256
+                  example: 'Schůze KS Pardubický kraj'
+                description:
+                  type: string
+                  maxLength: 1024
+                  example: ''
+                organizer:
+                  type: string
+                  maxLength: 256
+                  example: 'PKS Pardubický kraj'
+              required:
+                - type
+                - start
+                - name
+                - description
+      responses:
+        201:
+          description: Event created
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  id:
+                    type: integer
+                    description: Event id
+    get:
+      x-mojo-to: events#list
+      security:
+        - Bearer: ['optional', '*']
+      tags:
+        - events
+      summary: "Udalosti"
+      operationId: getEvents
+      parameters:
+      - $ref: '#/components/parameters/offset'
+      - $ref: '#/components/parameters/limit'
+      - name: type
+        in: query
+        description: "Typ udalosti"
+        required: false
+        schema:
+          type: integer
+          enum: [1, 2, 3]
+      - name: show_finished
+        in: query
+        description: "Zobrazovat ukoncene"
+        required: false
+        schema:
+          type: boolean
+      - name: sort
+        description: "Razeni"
+        in: query
+        style: form
+        schema:
+          type: array
+          uniqueItems: true
+          items:
+            type: string
+            enum: [ start, -start]
+          default: [ start ]
+      responses:
+        200:
+          description: Events
+          content:
+            application/json:
+              schema:
+                type: object
+                properties:
+                  count:
+                    type: integer
+                    description: Celkovy pocet
+                  data:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/EventInList'
+  /events/{id}:
+    get:
+      x-mojo-to: events#get
+      security:
+        - Bearer: ['optional']
+      tags:
+        - events
+      summary: "Detail udalosti"
+      operationId: getEvent
+      responses:
+        200:
+          description: Udalost
+          content:
+            application/json:
+              schema:
+                  $ref: '#/components/schemas/Event'
+    put:
+      x-mojo-to: events#update
+      security:
+        - Bearer: []
+      tags:
+        - events
+      summary: "Uprava udalosti"
+      operationId: updateEvent
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                type:
+                  type: integer
+                  enum: [1, 2, 3]
+                state:
+                  type: integer
+                  enum: [0, 1, 2]
+                is_published:
+                  type: boolean
+                start:
+                  type: string
+                  maxLength: 20
+                  example: '18.07.2021 10:00'
+                finish:
+                  type: string
+                  maxLength: 20
+                  example: '18.07.2021 18:00'
+                name:
+                  type: string
+                  maxLength: 256
+                  example: 'Schůze KS Pardubický kraj'
+                description:
+                  type: string
+                  maxLength: 1024
+                  example: ''
+                organizer:
+                  type: string
+                  maxLength: 256
+                  example: 'PKS Pardubický kraj'
+      responses:
+        204:
+          description: Event updated
+
+    delete:
+      x-mojo-to: events#delete
+      security:
+        - Bearer: ['organizer']
+      tags:
+        - event
+      summary: "Smazat udalost"
+      operationId: deleteEvent
+      responses:
+        204:
+          description: Event deleted
+
   /sso/subjects:
     get:
       x-mojo-to: SSO#subjects
diff --git a/sql/8/up.sql b/sql/8/up.sql
index dc274eeb59d372b49009520e7eccccdff28f6f49..dfeabb9186cb0ef4b2281597cf9afdc31bb9fdd6 100644
--- a/sql/8/up.sql
+++ b/sql/8/up.sql
@@ -2,9 +2,12 @@ create table "events" (
     "id" integer not null default nextval('uid_seq'),
     "uuid" uuid not null, -- unique string
     "type" smallint not null default 1, --1
+    "state" integer not null default 0,
     "owner_id" integer not null,
+    "is_published" bool not null default false,
     "start" timestamp(0),
     "finish" timestamp(0),
+    "deleted" timestamp(0),
     "name" text not null,
     "description" text,
     "organizer" text,