diff --git a/lib/CF/Controller/Posts.pm b/lib/CF/Controller/Posts.pm
index 17b4bc8e5290a8f2f43ade6ef1739d2b91dc2d95..eac8110effbc218ced88432ba93b1d9600a8a7e9 100644
--- a/lib/CF/Controller/Posts.pm
+++ b/lib/CF/Controller/Posts.pm
@@ -1,12 +1,18 @@
 package CF::Controller::Posts;
+
 use Mojo::Base 'Mojolicious::Controller';
+use Mojo::Pg::PubSub;
+use feature 'signatures';
+no warnings qw{ experimental::signatures };
+
+sub create ($c) {
+    $c->openapi->valid_input or return;
 
-sub create {
-    my $c    = shift->openapi->valid_input or return;
-    my $args = $c->req->json;
+    my $args   = $c->req->json;
+    my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg);
 
     # Navrh postupu muze predlozit jenom clen
-    if ( $args->{type} == 0 && ! $c->user_roles->{'xember'} ) {
+    if ( $args->{type} == 0 && ! $c->user_roles->{member} ) {
         return $c->error(401, 'Insufficient permissions');
     }
 
@@ -16,7 +22,9 @@ sub create {
         content => $args->{content},
     });
 
-    ### TODO: Notify
+    $pubsub->json('posts')->notify(
+        posts => $post->view->format()
+    );
 
     $c->render(
         status  => 201,
@@ -24,4 +32,171 @@ sub create {
     );
 };
 
+sub get ($c) {
+    $c->openapi->valid_input or return;
+
+    my $post = $c->schema->resultset('Post_view')->find($c->stash->{id});
+    return $c->error(404, 'Post not found') if ! $post;
+
+    $c->render(openapi => $c->spec_filter($post->format(), 'Post'));
+}
+
+sub list ($c) {
+    $c->openapi->valid_input or return;
+    my $args = $c->validation->output;
+
+    my ($cond, $attrs) = $c->search_parametrs( $args );
+
+    $cond->{type} = $args->{type};
+
+    if ( exists $args->{archived} && defined $args->{archived}) {
+        $cond->{is_archived} = $args->{archived};
+    }
+
+    # neschvalene navrhy postupu
+    if ( $args->{type} == 0 && ! $c->user_roles->{chairman} ) {
+        $cond->{state} = {'-in' => [1,2,3,4]}
+    }
+
+    my @posts = ();
+
+    my $count = $c->schema->resultset('Post_view')->count($cond);
+
+    if ( $count ) {
+        my $posts = $c->schema->resultset('Post_view')->search($cond, $attrs);
+
+        RECORD:
+        while ( my $post = $posts->next() ) {
+            push @posts, $c->spec_filter($post->format(), 'Post');
+        }
+    }
+
+    $c->render(json => {
+        data  => \@posts,
+        total => $count,
+    });
+}
+
+sub update ($c) {
+    $c->openapi->valid_input or return;
+
+    my $args   = $c->req->json;
+    my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg);
+    my $update = {};
+
+    my $post = $c->schema->resultset('Post')->find($c->stash->{id});
+    return $c->error(404, 'Post not found') if ! $post;
+
+    if ( ! $c->user_roles->{chairman} ) {
+        if ( $post->user_id != $c->user->{id} ) {
+            return $c->error(403, 'Access deined');
+        }
+        else {
+            delete $args->{is_archived};
+            delete $args->{state};
+        }
+    }
+
+    foreach my $key (qw(is_archoved state content)) {
+        $update->{$key} = $args->{$key} if exists $args->{$key};
+    }
+
+    my $guard = $c->schema->txn_scope_guard;
+
+    $post->add_to_history({
+        user_id  => $c->user->{id},
+        datetime => $post->datetime,
+        content  => $post->content,
+    });
+
+    $post->update( $update );
+
+    $pubsub->json('posts')->notify(
+        posts => $post->view->format()
+    );
+
+    $guard->commit;
+
+    $c->render(status => 204, text => '');
+}
+
+sub ranking ($c) {
+    $c->openapi->valid_input or return;
+    my $args = $c->validation->output;
+    my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg);
+
+    my $post = $c->schema->resultset('Post')->find($c->stash->{id});
+    return $c->error(404, 'Post not found') if ! $post;
+
+    my $user_ranking = $post->rankings({
+        user_id => $c->user->{id},
+    })->first;
+
+    my $update = {
+        ranking_likes    => $post->ranking_likes,
+        ranking_dislikes => $post->ranking_dislikes,
+    };
+
+    my $user_ranking_update;
+
+    if ($user_ranking && $user_ranking->ranking == $args->{ranking} ) {
+        $user_ranking_update = 0;
+    }
+    else {
+        $user_ranking_update = $args->{ranking};
+    }
+
+    if ($user_ranking && $user_ranking->ranking == 1 ) {
+        $update->{ranking_likes}--;
+        $update->{ranking_dislikes}++ if $args->{ranking} == -1;
+    }
+    elsif ($user_ranking && $user_ranking->ranking == -1 ) {
+        $update->{ranking_dislikes}--;
+        $update->{ranking_likes}++ if $args->{ranking} == 1;
+    }
+    else {
+        $update->{ranking_likes}++ if $args->{ranking} == 1;
+        $update->{ranking_dislikes}++ if $args->{ranking} == -1;
+    }
+
+    my $guard = $c->schema->txn_scope_guard;
+
+    $post->update( $update );
+
+    if ( $user_ranking ) {
+        $user_ranking->update( { ranking => $user_ranking_update } );
+    }
+    else {
+        $post->add_to_rankings({
+            user_id => $c->user->{id},
+            ranking => $user_ranking_update,
+        });
+    }
+
+    $pubsub->json('posts')->notify(
+        posts => $post->view->format()
+    );
+    $guard->commit;
+
+    $c->render(status => 204, text => '');
+}
+
+sub ws {
+    my $c = shift;
+
+    $c->inactivity_timeout(300);
+
+    my $pubsub = Mojo::Pg::PubSub->new(pg => $c->pg);
+
+    $pubsub->listen(posts => sub($pubsub, $payload) {
+        # FILTER?
+        $c->send($payload);
+    });
+
+    $c->on(finish => sub ($c, $code, $reason = undef) {
+        $pubsub->unlisten('streams');
+        $c->app->log->debug("WebSocket closed with status $code");
+    });
+}
+
 1;
diff --git a/lib/CF/Schema/Result/Post.pm b/lib/CF/Schema/Result/Post.pm
index 17ba0b9ecdfcc5889575c095d760f9760ce8b440..03d004d4ccc6b333fac72bd978aedeefeda6d380 100644
--- a/lib/CF/Schema/Result/Post.pm
+++ b/lib/CF/Schema/Result/Post.pm
@@ -30,5 +30,23 @@ __PACKAGE__->add_columns(
 
 __PACKAGE__->set_primary_key('id');
 
-1;
+__PACKAGE__->has_one( view => 'CF::Schema::Result::Post_view', 'id');
 
+__PACKAGE__->belongs_to(
+    user => 'Zircon::Schema::Result::User',
+    {
+        'foreign.id' => 'self.user_id',
+    },
+);
+
+__PACKAGE__->has_many(
+    history => 'CF::Schema::Result::PostHistory',
+    { 'foreign.post_id' => 'self.id', },
+);
+
+__PACKAGE__->has_many(
+    rankings => 'CF::Schema::Result::PostRanking',
+    { 'foreign.post_id' => 'self.id', },
+);
+
+1;
diff --git a/lib/CF/Schema/Result/PostHistory.pm b/lib/CF/Schema/Result/PostHistory.pm
new file mode 100644
index 0000000000000000000000000000000000000000..8888e5b72a2932031a0cdd288cc62ef2074ca7d1
--- /dev/null
+++ b/lib/CF/Schema/Result/PostHistory.pm
@@ -0,0 +1,45 @@
+package CF::Schema::Result::PostHistory;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+our $VERSION = 1;
+
+__PACKAGE__->table('posts_history');
+
+__PACKAGE__->add_columns(
+    id => {
+        data_type         => 'integer',
+        is_auto_increment => 1,
+        is_nullable       => 0,
+        sequence          => 'uid_seq'
+    },
+    qw(
+        datetime
+        user_id
+        post_id
+        content
+    ),
+);
+
+__PACKAGE__->set_primary_key('id');
+
+__PACKAGE__->has_one( view => 'CF::Schema::Result::Post_view', 'id');
+
+__PACKAGE__->belongs_to(
+    user => 'Zircon::Schema::Result::User',
+    {
+        'foreign.id' => 'self.user_id',
+    },
+);
+
+__PACKAGE__->belongs_to(
+    post => 'Zircon::Schema::Result::Post',
+    {
+        'foreign.id' => 'self.post_id',
+    },
+);
+
+1;
diff --git a/lib/CF/Schema/Result/PostRanking.pm b/lib/CF/Schema/Result/PostRanking.pm
new file mode 100644
index 0000000000000000000000000000000000000000..e55ad51a1cb54e902e38ee906220cdf4cfb7532f
--- /dev/null
+++ b/lib/CF/Schema/Result/PostRanking.pm
@@ -0,0 +1,48 @@
+package CF::Schema::Result::PostRanking;
+
+use strict;
+use warnings;
+
+use base 'DBIx::Class::Core';
+
+our $VERSION = 1;
+
+__PACKAGE__->table('posts_ranking');
+
+__PACKAGE__->add_columns(
+    id => {
+        data_type         => 'integer',
+        is_auto_increment => 1,
+        is_nullable       => 0,
+        sequence          => 'uid_seq'
+    },
+    qw(
+        user_id
+        post_id
+        ranking
+    ),
+);
+
+__PACKAGE__->set_primary_key('id');
+
+__PACKAGE__->add_unique_constraint(
+    'post_user' => [qw(user_id post_id)]
+);
+
+__PACKAGE__->has_one( view => 'CF::Schema::Result::Post_view', 'id');
+
+__PACKAGE__->belongs_to(
+    user => 'Zircon::Schema::Result::User',
+    {
+        'foreign.id' => 'self.user_id',
+    },
+);
+
+__PACKAGE__->belongs_to(
+    post => 'Zircon::Schema::Result::Post',
+    {
+        'foreign.id' => 'self.post_id',
+    },
+);
+
+1;
diff --git a/lib/CF/Schema/Result/Post_view.pm b/lib/CF/Schema/Result/Post_view.pm
index a72981125faa1b63416e9cce05dc9a04342d0595..4ddf5c77f494bd43c23b799c4ec4ded0783c5e30 100644
--- a/lib/CF/Schema/Result/Post_view.pm
+++ b/lib/CF/Schema/Result/Post_view.pm
@@ -11,7 +11,7 @@ __PACKAGE__->table('posts_view');
 
 __PACKAGE__->add_columns(
     qw(
-        score
+        ranking_score
         user_name
         group_name
     ),
@@ -23,18 +23,20 @@ sub format {
     my $self = shift;
 
     my $post = {
-        id         => $self->id,
-        datetime   => $self->datetime,
-        type       => $self->type,
-        is_archive => $self->is_archived,
+        id          => $self->id,
+        datetime    => $self->datetime,
+        type        => $self->type,
+        state       => $self->state,
+        content     => $self->content,
+        is_archived => $self->is_archived,
         author => {
             name  => $self->user_name,
             group => $self->group_name,
         },
         ranking => {
-            score    => $self->score,
-            likes    => $self->likes,
-            dislikes => $self->dislikes,
+            score    => $self->ranking_score,
+            likes    => $self->ranking_likes,
+            dislikes => $self->ranking_dislikes,
             my_vote  => 0, #TODO
         }
     };
diff --git a/openapi.yaml b/openapi.yaml
index 65e0749e56bd660823e44d7592cfb495a2d400a6..a54058b83a57ce8849513c3c3c329b5a33415f4f 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -24,6 +24,25 @@ components:
      name: Authorization
      in: header
      type: apiKey
+  parameters:
+    offset:
+      name: offset
+      in: query
+      description: The number of items to skip before starting to collect the result set
+      required: false
+      schema:
+        type: integer
+        default: 0
+        minimum: 0
+    limit:
+      name: limit
+      in: query
+      description: The numbers of items to return
+      required: false
+      schema:
+        type: integer
+        default: 100
+        minimum: 1
   schemas:
     ProgramScheduleEntry:
       type: object
@@ -67,53 +86,30 @@ components:
             type: string
         group:
             type: string
-    DiscussionPost:
+    Post:
       type: object
-      description: Prispevek do rozpravy
+      description: Prispevek
       properties:
         id:
           type: integer
           readOnly: true
         datetime:
           type: string
-        is_archived:
-          type: boolean
-        content:
-          type: string
-        author:
-            $ref: '#/components/schemas/Author'
-        ranking:
-            $ref: '#/components/schemas/Ranking'
-    ProposalPost:
-      type: object
-      description: NavrhPostupu
-      properties:
-        id:
+        type:
           type: integer
           readOnly: true
-        datetime:
+          enum: [0, 1]
+        state:
+          type: integer
+          enum: [0, 1, 2, 3, 4]
+        content:
           type: string
         is_archived:
           type: boolean
-        content:
-          type: string
         author:
             $ref: '#/components/schemas/Author'
         ranking:
             $ref: '#/components/schemas/Ranking'
-#  responses:
-#    Unauthorized:
-#      description: Unauthorized
-#      schema:
-#        $ref: '#/definitions/ErrorResponse'
-#    Forbidden:
-#      description: Forbidden
-#      schema:
-#        $ref: '#/definitions/ErrorResponse'
-#    NotFound:
-#      description: The specified resource was not found
-#      schema:
-#        $ref: '#/definitions/ErrorResponse'
 
 paths:
   /program:
@@ -166,23 +162,133 @@ paths:
                   id:
                     type: integer
                     description: Post id
-
     get:
       x-mojo-to: posts#list
       security:
-        - Bearer: []
+        - Bearer: ['optional', 'member', 'regp']
       tags:
         - posts
       summary: "Zpravy"
       operationId: getPosts
+      parameters:
+      - $ref: '#/components/parameters/offset'
+      - $ref: '#/components/parameters/limit'
+      - name: type
+        in: query
+        description: "Typ zpravy"
+        required: true
+        schema:
+          type: integer
+          enum: [0, 1]
+      - name: archived
+        in: query
+        description: "Zobrazovat archivovane"
+        required: false
+        schema:
+          type: boolean
+      - name: sort
+        description: "Razeni"
+        in: query
+        style: form
+        schema:
+          type: array
+          uniqueItems: true
+          items:
+            type: string
+            enum: [ type, datetime, -type, -datetime,]
+          default: [ -datetime ]
       responses:
         200:
           description: Posts
           content:
             application/json:
               schema:
-                type: array
-                items:
-                  oneOf:
-                   - $ref: '#/components/schemas/DiscussionPost'
-                   - $ref: '#/components/schemas/ProposalPost'
+                type: object
+                properties:
+                  count:
+                    type: integer
+                    description: Celkovy pocet
+                  data:
+                    type: array
+                    items:
+                      $ref: '#/components/schemas/Post'
+  /posts/{id}:
+    get:
+      x-mojo-to: posts#get
+      tags:
+        - posts
+      summary: "Detail zpravy"
+      operationId: getPost
+      responses:
+        200:
+          description: Post
+          content:
+            application/json:
+              schema:
+                  $ref: '#/components/schemas/Post'
+    put:
+      x-mojo-to: posts#update
+      security:
+        - Bearer: ['member', 'regp']
+      tags:
+        - posts
+      summary: "Uprava zpravu"
+      operationId: updatePost
+      requestBody:
+        content:
+          application/json:
+            schema:
+              type: object
+              properties:
+                state:
+                  type: integer
+                  enum: [0, 1, 2, 3, 4]
+                content:
+                  type: string
+                isr_archived:
+                  type: boolean
+      responses:
+        204:
+          description: Post updated
+
+  /posts/{id}/like:
+    patch:
+      security:
+        - Bearer: ['member', 'regp']
+      x-mojo-to: posts#ranking
+      tags:
+        - posts
+      summary: "Like"
+      operationId: likePost
+      parameters:
+      - name: ranking
+        in: query
+        required: false
+        schema:
+          type: integer
+          enum: [1]
+          default: 1
+      responses:
+        204:
+          description: Post liked
+
+  /posts/{id}/dislike:
+    patch:
+      security:
+        - Bearer: ['member', 'regp']
+      x-mojo-to: posts#ranking
+      tags:
+        - posts
+      summary: "Like"
+      operationId: dislikePost
+      parameters:
+      - name: ranking
+        in: query
+        required: false
+        schema:
+          type: integer
+          enum: [-1]
+          default: -1
+      responses:
+        204:
+          description: Post disliked
diff --git a/sql/2/up.sql b/sql/2/up.sql
index 5f98dd1bb32553fed86c52b3308a38cde766c855..462490230544280bf52d362905cd49f14f6e168e 100644
--- a/sql/2/up.sql
+++ b/sql/2/up.sql
@@ -26,6 +26,7 @@ create table "announcements" (
 create view "posts_view" as
 select
     "posts".*,
+    "posts"."ranking_likes" - "posts"."ranking_dislikes" as "ranking_score",
     "users"."name" as "user_name",
     "users"."main_group_name" as "group_name"
 from posts
diff --git a/sql/3/up.sql b/sql/3/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..58d40fa56f1596bae2d34b4e5add6f72178e2d88
--- /dev/null
+++ b/sql/3/up.sql
@@ -0,0 +1,17 @@
+create table "posts_history" (
+    "id" integer not null default nextval('uid_seq'),
+    "datetime" timestamp(0) not null default now(),
+    "post_id" integer not null,
+    "user_id" integer not null,
+    "content" text,
+    primary key("id")
+);
+
+create table "posts_ranking" (
+    "id" integer not null default nextval('uid_seq'),
+    "post_id" integer not null,
+    "user_id" integer not null,
+    "ranking" integer,
+    primary key("id"),
+    unique("post_id", "user_id")
+);