thingsboard-developers

Initial commit

12/1/2016 7:38:15 AM

Changes

.gitignore 32(+32 -0)

application/pom.xml 417(+417 -0)

common/data/pom.xml 76(+76 -0)

common/pom.xml 43(+43 -0)

LICENSE 201(+201 -0)

pom.xml 710(+710 -0)

README.md 29(+29 -0)

tools/pom.xml 83(+83 -0)

transport/pom.xml 50(+50 -0)

Details

.gitignore 32(+32 -0)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e14c866
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+output/**
+*.class
+*~
+*.iml
+*/.idea/**
+.idea/**
+.idea
+*.log
+*.log.[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]
+*/.classpath
+.classpath
+*/.project
+.project
+.cache/**
+target/
+build/
+tmp_deb_control/
+tmp_rpm_control/
+tmp_sh/
+.gwt/
+.settings/
+/bin
+bin/
+**/dependency-reduced-pom.xml
+pom.xml.versionsBackup
+.DS_Store
+**/.gradle
+**/local.properties
+**/build
+**/target
+**/Californium.properties
+**/.env
diff --git a/application/.gitignore b/application/.gitignore
new file mode 100644
index 0000000..08eb0a0
--- /dev/null
+++ b/application/.gitignore
@@ -0,0 +1 @@
+!bin/
\ No newline at end of file
diff --git a/application/build.gradle b/application/build.gradle
new file mode 100644
index 0000000..833cf4c
--- /dev/null
+++ b/application/build.gradle
@@ -0,0 +1,134 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+buildscript {
+    ext {
+        osPackageVersion = "3.8.0"
+    }
+    repositories {
+        jcenter()
+    }
+    dependencies {
+        classpath("com.netflix.nebula:gradle-ospackage-plugin:${osPackageVersion}")
+    }
+}
+
+apply plugin: "nebula.ospackage"
+
+buildDir = projectBuildDir
+version = projectVersion
+distsDirName = "./"
+
+// OS Package plugin configuration
+ospackage {
+    packageName = pkgName
+    version = "${project.version}"
+    release = 1
+    os = LINUX
+    type = BINARY
+
+    into pkgInstallFolder
+
+    user pkgName
+    permissionGroup pkgName
+
+    // Copy the actual .jar file
+    from(mainJar) {
+        // Strip the version from the jar filename
+        rename { String fileName ->
+            fileName.replace("-${project.version}", "")
+        }
+        fileMode 0500
+        into "bin"
+    }
+
+    // Copy the config files
+    from("target/conf") {
+        fileType CONFIG | NOREPLACE
+        fileMode 0754
+        into "conf"
+    }
+
+    // Copy the data files
+    from("target/data") {
+        fileType CONFIG | NOREPLACE
+        fileMode 0754
+        into "data"
+    }
+
+    // Copy the extensions files
+    from("target/extensions") {
+        into "extensions"
+    }
+}
+
+// Configure our RPM build task
+buildRpm {
+
+    arch = NOARCH
+
+    version = projectVersion.replace('-', '')
+    archiveName = "${pkgName}.rpm"
+
+    requires("java-1.8.0")
+
+    preInstall file("${buildDir}/control/rpm/preinst")
+    postInstall file("${buildDir}/control/rpm/postinst")
+    preUninstall file("${buildDir}/control/rpm/prerm")
+    postUninstall file("${buildDir}/control/rpm/postrm")
+
+    user pkgName
+    permissionGroup pkgName
+
+    // Copy the system unit files
+    from("${buildDir}/control/${pkgName}.service") {
+        addParentDirs = false
+        fileMode 0644
+        into "/usr/lib/systemd/system"
+    }
+
+    directory(pkgLogFolder, 0755)
+    link("${pkgInstallFolder}/bin/${pkgName}.yml", "${pkgInstallFolder}/conf/${pkgName}.yml")
+    link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
+}
+
+// Same as the buildRpm task
+buildDeb {
+
+    arch = "all"
+
+    archiveName = "${pkgName}.deb"
+
+    requires("openjdk-8-jre").or("java8-runtime").or("oracle-java8-installer")
+
+    configurationFile("${pkgInstallFolder}/conf/${pkgName}.conf")
+    configurationFile("${pkgInstallFolder}/conf/${pkgName}.yml")
+    configurationFile("${pkgInstallFolder}/conf/logback.xml")
+    configurationFile("${pkgInstallFolder}/conf/actor-system.conf")
+
+    preInstall file("${buildDir}/control/deb/preinst")
+    postInstall file("${buildDir}/control/deb/postinst")
+    preUninstall file("${buildDir}/control/deb/prerm")
+    postUninstall file("${buildDir}/control/deb/postrm")
+
+    user pkgName
+    permissionGroup pkgName
+
+    directory(pkgLogFolder, 0755)
+    link("/etc/init.d/${pkgName}", "${pkgInstallFolder}/bin/${pkgName}.jar")
+    link("${pkgInstallFolder}/bin/${pkgName}.yml", "${pkgInstallFolder}/conf/${pkgName}.yml")
+    link("/etc/${pkgName}/conf", "${pkgInstallFolder}/conf")
+}

application/pom.xml 417(+417 -0)

diff --git a/application/pom.xml b/application/pom.xml
new file mode 100644
index 0000000..d75ece1
--- /dev/null
+++ b/application/pom.xml
@@ -0,0 +1,417 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>server</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server</groupId>
+    <artifactId>application</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Application</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/..</main.dir>
+        <pkg.name>thingsboard</pkg.name>
+        <pkg.logFolder>/var/log/${pkg.name}</pkg.logFolder>
+        <pkg.installFolder>/usr/share/${pkg.name}</pkg.installFolder>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-transport-native-epoll</artifactId>
+            <version>${netty.version}</version>
+            <!-- Explicitly bring in the linux classifier, test may fail on 32-bit linux -->
+            <classifier>linux-x86_64</classifier>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server</groupId>
+            <artifactId>extensions-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server</groupId>
+            <artifactId>extensions-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>transport</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server.transport</groupId>
+            <artifactId>http</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server.transport</groupId>
+            <artifactId>coap</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server.transport</groupId>
+            <artifactId>mqtt</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server</groupId>
+            <artifactId>dao</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server</groupId>
+            <artifactId>dao</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>io.takari.junit</groupId>
+            <artifactId>takari-cpsuite</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.cassandraunit</groupId>
+            <artifactId>cassandra-unit</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-log4j12</artifactId>
+                </exclusion>
+            </exclusions>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server</groupId>
+            <artifactId>ui</artifactId>
+            <version>${project.version}</version>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-websocket</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.jsonwebtoken</groupId>
+            <artifactId>jjwt</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>joda-time</groupId>
+            <artifactId>joda-time</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-tools</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.security</groupId>
+            <artifactId>spring-security-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jayway.jsonpath</groupId>
+            <artifactId>json-path</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.jayway.jsonpath</groupId>
+            <artifactId>json-path-assert</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.typesafe.akka</groupId>
+            <artifactId>akka-actor_${scala.version}</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.typesafe.akka</groupId>
+            <artifactId>akka-slf4j_${scala.version}</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.curator</groupId>
+            <artifactId>curator-recipes</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.grpc</groupId>
+            <artifactId>grpc-netty</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.grpc</groupId>
+            <artifactId>grpc-protobuf</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.grpc</groupId>
+            <artifactId>grpc-stub</artifactId>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${pkg.name}-${project.version}</finalName>
+        <resources>
+            <resource>
+                <directory>${project.basedir}/src/main/resources</directory>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>${surfire.version}</version>
+                <configuration>
+                    <systemPropertyVariables>
+                        <spring.config.name>thingsboard</spring.config.name>
+                    </systemPropertyVariables>
+                    <includes>
+                        <include>**/*TestSuite.java</include>
+                    </includes>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-resources-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-conf</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/conf</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/resources</directory>
+                                    <excludes>
+                                        <exclude>logback.xml</exclude>
+                                    </excludes>
+                                    <filtering>false</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-service-conf</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/conf</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/conf</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-control</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/control</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>src/main/scripts/control</directory>
+                                    <filtering>true</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                    <execution>
+                        <id>copy-data-cql</id>
+                        <phase>process-resources</phase>
+                        <goals>
+                            <goal>copy-resources</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/data</outputDirectory>
+                            <resources>
+                                <resource>
+                                    <directory>../dao/src/main/resources</directory>
+                                    <includes>
+                                        <include>**/*.cql</include>
+                                    </includes>
+                                    <filtering>false</filtering>
+                                </resource>
+                            </resources>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>copy-extensions</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>copy</goal>
+                        </goals>
+                        <configuration>
+                            <outputDirectory>${project.build.directory}/extensions</outputDirectory>
+                            <artifactItems>
+                                <artifactItem>
+                                    <groupId>org.thingsboard.server.extensions</groupId>
+                                    <artifactId>extension-rabbitmq</artifactId>
+                                    <classifier>extension</classifier>
+                                </artifactItem>
+                                <artifactItem>
+                                    <groupId>org.thingsboard.server.extensions</groupId>
+                                    <artifactId>extension-rest-api-call</artifactId>
+                                    <classifier>extension</classifier>
+                                </artifactItem>
+                                <artifactItem>
+                                    <groupId>org.thingsboard.server.extensions</groupId>
+                                    <artifactId>extension-kafka</artifactId>
+                                    <classifier>extension</classifier>
+                                </artifactItem>
+                            </artifactItems>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-jar-plugin</artifactId>
+                <configuration>
+                    <archive>
+                        <manifestEntries>
+                            <Implementation-Title>Thingsboard</Implementation-Title>
+                            <Implementation-Version>${project.version}</Implementation-Version>
+                        </manifestEntries>
+                    </archive>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <layout>ZIP</layout>
+                    <executable>true</executable>
+                    <excludeDevtools>true</excludeDevtools>
+                    <embeddedLaunchScriptProperties>
+                        <confFolder>${pkg.installFolder}/conf</confFolder>
+                        <logFolder>${pkg.logFolder}</logFolder>
+                        <logFilename>${pkg.name}.out</logFilename>
+                    </embeddedLaunchScriptProperties>
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.fortasoft</groupId>
+                <artifactId>gradle-maven-plugin</artifactId>
+                <configuration>
+                    <tasks>
+                        <task>build</task>
+                        <task>buildDeb</task>
+                        <task>buildRpm</task>
+                    </tasks>
+                    <args>
+                        <arg>-PprojectBuildDir=${project.build.directory}</arg>
+                        <arg>-PprojectVersion=${project.version}</arg>
+                        <arg>-PmainJar=${project.build.directory}/${project.build.finalName}.${project.packaging}</arg>
+                        <arg>-PpkgName=${pkg.name}</arg>
+                        <arg>-PpkgInstallFolder=${pkg.installFolder}</arg>
+                        <arg>-PpkgLogFolder=${pkg.logFolder}</arg>
+                    </args>
+                </configuration>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>invoke</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.xolstice.maven.plugins</groupId>
+                <artifactId>protobuf-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/application/src/main/conf/logback.xml b/application/src/main/conf/logback.xml
new file mode 100644
index 0000000..4187356
--- /dev/null
+++ b/application/src/main/conf/logback.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+    <appender name="fileLogAppender"
+              class="ch.qos.logback.core.rolling.RollingFileAppender">
+        <file>${pkg.logFolder}/${pkg.name}.log</file>
+        <rollingPolicy
+                class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
+            <fileNamePattern>${pkg.name}.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
+            <maxFileSize>100MB</maxFileSize>
+            <maxHistory>30</maxHistory>
+            <totalSizeCap>3GB</totalSizeCap>
+        </rollingPolicy>
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.thingsboard.server" level="INFO" />
+    <logger name="akka" level="INFO" />
+
+    <root level="INFO">
+        <appender-ref ref="fileLogAppender"/>
+    </root>
+
+</configuration>
diff --git a/application/src/main/conf/thingsboard.conf b/application/src/main/conf/thingsboard.conf
new file mode 100644
index 0000000..dc161c0
--- /dev/null
+++ b/application/src/main/conf/thingsboard.conf
@@ -0,0 +1,19 @@
+#
+# Copyright © 2016 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+export JAVA_OPTS="$JAVA_OPTS"
+export LOG_FILENAME=${pkg.name}.out
+export LOADER_PATH=${pkg.installFolder}/conf,${pkg.installFolder}/extensions
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
new file mode 100644
index 0000000..6b5dd56
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -0,0 +1,191 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors;
+
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.actor.Scheduler;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import org.thingsboard.server.service.component.ComponentDiscoveryService;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Optional;
+
+@Component
+public class ActorSystemContext {
+    private static final String AKKA_CONF_FILE_NAME = "actor-system.conf";
+
+    protected final ObjectMapper mapper = new ObjectMapper();
+
+    @Getter @Setter private ActorService actorService;
+
+    @Autowired
+    @Getter private DiscoveryService discoveryService;
+
+    @Autowired
+    @Getter @Setter private ComponentDiscoveryService componentService;
+
+    @Autowired
+    @Getter private ClusterRoutingService routingService;
+
+    @Autowired
+    @Getter private ClusterRpcService rpcService;
+
+    @Autowired
+    @Getter private DeviceAuthService deviceAuthService;
+
+    @Autowired
+    @Getter private DeviceService deviceService;
+
+    @Autowired
+    @Getter private TenantService tenantService;
+
+    @Autowired
+    @Getter private CustomerService customerService;
+
+    @Autowired
+    @Getter private RuleService ruleService;
+
+    @Autowired
+    @Getter private PluginService pluginService;
+
+    @Autowired
+    @Getter private TimeseriesService tsService;
+
+    @Autowired
+    @Getter private AttributesService attributesService;
+
+    @Autowired
+    @Getter private EventService eventService;
+
+    @Autowired
+    @Getter @Setter private PluginWebSocketMsgEndpoint wsMsgEndpoint;
+
+    @Value("${actors.session.sync.timeout}")
+    @Getter private long syncSessionTimeout;
+
+    @Value("${actors.plugin.termination.delay}")
+    @Getter private long pluginActorTerminationDelay;
+
+    @Value("${actors.plugin.processing.timeout}")
+    @Getter private long pluginProcessingTimeout;
+
+    @Value("${actors.plugin.error_persist_frequency}")
+    @Getter private long pluginErrorPersistFrequency;
+
+    @Value("${actors.rule.termination.delay}")
+    @Getter private long ruleActorTerminationDelay;
+
+    @Value("${actors.rule.error_persist_frequency}")
+    @Getter private long ruleErrorPersistFrequency;
+
+    @Value("${actors.statistics.enabled}")
+    @Getter private boolean statisticsEnabled;
+
+    @Value("${actors.statistics.persist_frequency}")
+    @Getter private long statisticsPersistFrequency;
+
+    @Getter @Setter private ActorSystem actorSystem;
+
+    @Getter @Setter private ActorRef appActor;
+
+    @Getter @Setter private ActorRef sessionManagerActor;
+
+    @Getter @Setter private ActorRef statsActor;
+
+    @Getter private final Config config;
+
+    public ActorSystemContext() {
+        config = ConfigFactory.parseResources(AKKA_CONF_FILE_NAME).withFallback(ConfigFactory.load());
+    }
+
+    public Scheduler getScheduler() {
+        return actorSystem.scheduler();
+    }
+
+    public void persistError(TenantId tenantId, EntityId entityId, String method, Exception e) {
+        Event event = new Event();
+        event.setTenantId(tenantId);
+        event.setEntityId(entityId);
+        event.setType(DataConstants.ERROR);
+        event.setBody(toBodyJson(discoveryService.getCurrentServer().getServerAddress(), method, toString(e)));
+        persistEvent(event);
+    }
+
+    public void persistLifecycleEvent(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent lcEvent, Exception e) {
+        Event event = new Event();
+        event.setTenantId(tenantId);
+        event.setEntityId(entityId);
+        event.setType(DataConstants.LC_EVENT);
+        event.setBody(toBodyJson(discoveryService.getCurrentServer().getServerAddress(), lcEvent, Optional.ofNullable(e)));
+        persistEvent(event);
+    }
+
+    private void persistEvent(Event event) {
+        eventService.save(event);
+    }
+
+    private String toString(Exception e) {
+        StringWriter sw = new StringWriter();
+        e.printStackTrace(new PrintWriter(sw));
+        return sw.toString();
+    }
+
+    private JsonNode toBodyJson(ServerAddress server, ComponentLifecycleEvent event, Optional<Exception> e) {
+        ObjectNode node = mapper.createObjectNode().put("server", server.toString()).put("event", event.name());
+        if (e.isPresent()) {
+            node = node.put("success", false);
+            node = node.put("error", toString(e.get()));
+        } else {
+            node = node.put("success", true);
+        }
+        return node;
+    }
+
+    private JsonNode toBodyJson(ServerAddress server, String method, String body) {
+        return mapper.createObjectNode().put("server", server.toString()).put("method", method).put("error", body);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
new file mode 100644
index 0000000..c370616
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -0,0 +1,226 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.app;
+
+import akka.actor.*;
+import akka.actor.SupervisorStrategy.Directive;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import akka.japi.Function;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.plugin.PluginManager;
+import org.thingsboard.server.actors.shared.plugin.SystemPluginManager;
+import org.thingsboard.server.actors.shared.rule.RuleManager;
+import org.thingsboard.server.actors.shared.rule.SystemRuleManager;
+import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.actors.tenant.TenantActor;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
+import scala.concurrent.duration.Duration;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class AppActor extends ContextAwareActor {
+
+    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+    public static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
+    private final RuleManager ruleManager;
+    private final PluginManager pluginManager;
+    private final TenantService tenantService;
+    private final Map<TenantId, ActorRef> tenantActors;
+
+    private AppActor(ActorSystemContext systemContext) {
+        super(systemContext);
+        this.ruleManager = new SystemRuleManager(systemContext);
+        this.pluginManager = new SystemPluginManager(systemContext);
+        this.tenantService = systemContext.getTenantService();
+        this.tenantActors = new HashMap<>();
+    }
+
+    @Override
+    public SupervisorStrategy supervisorStrategy() {
+        return strategy;
+    }
+
+    @Override
+    public void preStart() {
+        logger.info("Starting main system actor.");
+        try {
+            ruleManager.init(this.context());
+            pluginManager.init(this.context());
+
+            PageDataIterable<Tenant> tenantIterator = new PageDataIterable<>(link -> tenantService.findTenants(link), ENTITY_PACK_LIMIT);
+            for (Tenant tenant : tenantIterator) {
+                logger.debug("[{}] Creating tenant actor", tenant.getId());
+                getOrCreateTenantActor(tenant.getId());
+                logger.debug("Tenant actor created.");
+            }
+
+            logger.info("Main system actor started.");
+        } catch (Exception e) {
+            logger.error(e, "Unknown failure");
+        }
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        logger.debug("Received message: {}", msg);
+        if (msg instanceof ToDeviceActorMsg) {
+            processDeviceMsg((ToDeviceActorMsg) msg);
+        } else if (msg instanceof ToPluginActorMsg) {
+            onToPluginMsg((ToPluginActorMsg) msg);
+        } else if (msg instanceof ToRuleActorMsg) {
+            onToRuleMsg((ToRuleActorMsg) msg);
+        } else if (msg instanceof ToDeviceActorNotificationMsg) {
+            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
+        } else if (msg instanceof Terminated) {
+            processTermination((Terminated) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            broadcast(msg);
+        } else if (msg instanceof ComponentLifecycleMsg) {
+            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+        } else if (msg instanceof PluginTerminationMsg) {
+            onPluginTerminated((PluginTerminationMsg) msg);
+        } else {
+            logger.warning("Unknown message: {}!", msg);
+        }
+    }
+
+    private void onPluginTerminated(PluginTerminationMsg msg) {
+        pluginManager.remove(msg.getId());
+    }
+
+    private void broadcast(Object msg) {
+        pluginManager.broadcast(msg);
+        tenantActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+    }
+
+    private void onToRuleMsg(ToRuleActorMsg msg) {
+        ActorRef target;
+        if (SYSTEM_TENANT.equals(msg.getTenantId())) {
+            target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
+        } else {
+            target = getOrCreateTenantActor(msg.getTenantId());
+        }
+        target.tell(msg, ActorRef.noSender());
+    }
+
+    private void onToPluginMsg(ToPluginActorMsg msg) {
+        ActorRef target;
+        if (SYSTEM_TENANT.equals(msg.getPluginTenantId())) {
+            target = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
+        } else {
+            target = getOrCreateTenantActor(msg.getPluginTenantId());
+        }
+        target.tell(msg, ActorRef.noSender());
+    }
+
+    private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
+        ActorRef target = null;
+        if (SYSTEM_TENANT.equals(msg.getTenantId())) {
+            if (msg.getPluginId().isPresent()) {
+                target = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId().get());
+            } else if (msg.getRuleId().isPresent()) {
+                Optional<ActorRef> ref = ruleManager.update(this.context(), msg.getRuleId().get(), msg.getEvent());
+                if (ref.isPresent()) {
+                    target = ref.get();
+                } else {
+                    logger.debug("Failed to find actor for rule: [{}]", msg.getRuleId());
+                    return;
+                }
+            }
+        } else {
+            target = getOrCreateTenantActor(msg.getTenantId());
+        }
+        if (target != null) {
+            target.tell(msg, ActorRef.noSender());
+        }
+    }
+
+    private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
+        getOrCreateTenantActor(msg.getTenantId()).tell(msg, ActorRef.noSender());
+    }
+
+    private void processDeviceMsg(ToDeviceActorMsg toDeviceActorMsg) {
+        TenantId tenantId = toDeviceActorMsg.getTenantId();
+        ActorRef tenantActor = getOrCreateTenantActor(tenantId);
+        if (toDeviceActorMsg.getPayload().getMsgType().requiresRulesProcessing()) {
+            tenantActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, ruleManager.getRuleChain()), context().self());
+        } else {
+            tenantActor.tell(toDeviceActorMsg, context().self());
+        }
+    }
+
+    private ActorRef getOrCreateTenantActor(TenantId tenantId) {
+        ActorRef tenantActor = tenantActors.get(tenantId);
+        if (tenantActor == null) {
+            tenantActor = context().actorOf(Props.create(new TenantActor.ActorCreator(systemContext, tenantId))
+                    .withDispatcher(DefaultActorService.CORE_DISPATCHER_NAME), tenantId.toString());
+            tenantActors.put(tenantId, tenantActor);
+        }
+        return tenantActor;
+    }
+
+    private void processTermination(Terminated message) {
+        ActorRef terminated = message.actor();
+        if (terminated instanceof LocalActorRef) {
+            logger.debug("Removed actor: {}", terminated);
+        } else {
+            throw new IllegalStateException("Remote actors are not supported!");
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<AppActor> {
+        private static final long serialVersionUID = 1L;
+
+        public ActorCreator(ActorSystemContext context) {
+            super(context);
+        }
+
+        @Override
+        public AppActor create() throws Exception {
+            return new AppActor(context);
+        }
+    }
+
+    private final SupervisorStrategy strategy = new OneForOneStrategy(3, Duration.create("1 minute"), new Function<Throwable, Directive>() {
+        @Override
+        public Directive apply(Throwable t) {
+            logger.error(t, "Unknown failure");
+            if (t instanceof RuntimeException) {
+                return SupervisorStrategy.restart();
+            } else {
+                return SupervisorStrategy.stop();
+            }
+        }
+    });
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
new file mode 100644
index 0000000..8b669e9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActor.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.device;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.rule.RulesProcessedMsg;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+
+public class DeviceActor extends ContextAwareActor {
+
+    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+    private final TenantId tenantId;
+    private final DeviceId deviceId;
+    private final DeviceActorMessageProcessor processor;
+
+    private DeviceActor(ActorSystemContext systemContext, TenantId tenantId, DeviceId deviceId) {
+        super(systemContext);
+        this.tenantId = tenantId;
+        this.deviceId = deviceId;
+        this.processor = new DeviceActorMessageProcessor(systemContext, logger, deviceId);
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (msg instanceof RuleChainDeviceMsg) {
+            processor.process(context(), (RuleChainDeviceMsg) msg);
+        } else if (msg instanceof RulesProcessedMsg) {
+            processor.onRulesProcessedMsg(context(), (RulesProcessedMsg) msg);
+        } else if (msg instanceof ToDeviceActorMsg) {
+            processor.process(context(), (ToDeviceActorMsg) msg);
+        } else if (msg instanceof ToDeviceActorNotificationMsg) {
+            if (msg instanceof DeviceAttributesEventNotificationMsg) {
+                processor.processAttributesUpdate(context(), (DeviceAttributesEventNotificationMsg) msg);
+            } else if (msg instanceof ToDeviceRpcRequestPluginMsg) {
+                processor.processRpcRequest(context(), (ToDeviceRpcRequestPluginMsg) msg);
+            }
+        } else if (msg instanceof TimeoutMsg) {
+            processor.processTimeout(context(), (TimeoutMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            processor.processClusterEventMsg((ClusterEventMsg) msg);
+        } else {
+            logger.debug("[{}][{}] Unknown msg type.", tenantId, deviceId, msg.getClass().getName());
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<DeviceActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final DeviceId deviceId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, DeviceId deviceId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.deviceId = deviceId;
+        }
+
+        @Override
+        public DeviceActor create() throws Exception {
+            return new DeviceActor(context, tenantId, deviceId);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
new file mode 100644
index 0000000..3949691
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java
@@ -0,0 +1,366 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.device;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.rule.ChainProcessingContext;
+import org.thingsboard.server.actors.rule.ChainProcessingMetaData;
+import org.thingsboard.server.actors.rule.RuleProcessingMsg;
+import org.thingsboard.server.actors.rule.RulesProcessedMsg;
+import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
+import org.thingsboard.server.actors.tenant.RuleChainDeviceMsg;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.AttributesUpdateNotification;
+import org.thingsboard.server.common.msg.core.BasicCommandAckResponse;
+import org.thingsboard.server.common.msg.core.BasicToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceRpcResponseMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutIntMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor {
+
+    private final DeviceId deviceId;
+    private final Map<SessionId, SessionInfo> attributeSubscriptions;
+    private final Map<SessionId, SessionInfo> rpcSubscriptions;
+
+    private final Map<Integer, ToDeviceRpcRequestMetadata> rpcPendingMap;
+
+    private int rpcSeq = 0;
+    private DeviceAttributes deviceAttributes;
+
+    public DeviceActorMessageProcessor(ActorSystemContext systemContext, LoggingAdapter logger, DeviceId deviceId) {
+        super(systemContext, logger);
+        this.deviceId = deviceId;
+        this.attributeSubscriptions = new HashMap<>();
+        this.rpcSubscriptions = new HashMap<>();
+        this.rpcPendingMap = new HashMap<>();
+        refreshAttributes();
+    }
+
+    private void refreshAttributes() {
+        this.deviceAttributes = new DeviceAttributes(fetchAttributes(DataConstants.CLIENT_SCOPE),
+                fetchAttributes(DataConstants.SERVER_SCOPE), fetchAttributes(DataConstants.SHARED_SCOPE));
+    }
+
+    void processRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg) {
+        ToDeviceRpcRequest request = msg.getMsg();
+        ToDeviceRpcRequestBody body = request.getBody();
+        ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
+                rpcSeq++,
+                body.getMethod(),
+                body.getParams()
+        );
+
+        long timeout = request.getExpirationTime() - System.currentTimeMillis();
+        if (timeout <= 0) {
+            logger.debug("[{}][{}] Ignoring message due to exp time reached", deviceId, request.getId(), request.getExpirationTime());
+            return;
+        }
+
+        boolean sent = rpcSubscriptions.size() > 0;
+        Set<SessionId> syncSessionSet = new HashSet<>();
+        rpcSubscriptions.entrySet().forEach(sub -> {
+            ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sub.getKey());
+            sendMsgToSessionActor(response, sub.getValue().getServer());
+            if (SessionType.SYNC == sub.getValue().getType()) {
+                syncSessionSet.add(sub.getKey());
+            }
+        });
+        syncSessionSet.forEach(rpcSubscriptions::remove);
+
+        if (request.isOneway() && sent) {
+            ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(msg, (String) null);
+            context.parent().tell(responsePluginMsg, ActorRef.noSender());
+            logger.debug("[{}] Rpc command response sent [{}]!", deviceId, request.getId());
+        } else {
+            registerPendingRpcRequest(context, msg, sent, rpcRequest, timeout);
+        }
+        if (sent) {
+            logger.debug("[{}] RPC request {} is sent!", deviceId, request.getId());
+        } else {
+            logger.debug("[{}] RPC request {} is NOT sent!", deviceId, request.getId());
+        }
+
+    }
+
+    private void registerPendingRpcRequest(ActorContext context, ToDeviceRpcRequestPluginMsg msg, boolean sent, ToDeviceRpcRequestMsg rpcRequest, long timeout) {
+        rpcPendingMap.put(rpcRequest.getRequestId(), new ToDeviceRpcRequestMetadata(msg, sent));
+        TimeoutIntMsg timeoutMsg = new TimeoutIntMsg(rpcRequest.getRequestId(), timeout);
+        scheduleMsgWithDelay(context, timeoutMsg, timeoutMsg.getTimeout());
+    }
+
+    public void processTimeout(ActorContext context, TimeoutMsg msg) {
+        ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.remove(msg.getId());
+        if (requestMd != null) {
+            logger.debug("[{}] RPC request [{}] timeout detected!", deviceId, msg.getId());
+            ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(requestMd.getMsg(), requestMd.isSent() ? RpcError.TIMEOUT : RpcError.NO_ACTIVE_CONNECTION);
+            context.parent().tell(responsePluginMsg, ActorRef.noSender());
+        }
+    }
+
+    private void sendPendingRequests(ActorContext context, SessionId sessionId, SessionType type, Optional<ServerAddress> server) {
+        if (!rpcPendingMap.isEmpty()) {
+            logger.debug("[{}] Pushing {} pending RPC messages to new async session [{}]", deviceId, rpcPendingMap.size(), sessionId);
+            if (type == SessionType.SYNC) {
+                logger.debug("[{}] Cleanup sync rpc session [{}]", deviceId, sessionId);
+                rpcSubscriptions.remove(sessionId);
+            }
+        } else {
+            logger.debug("[{}] No pending RPC messages for new async session [{}]", deviceId, sessionId);
+        }
+        Set<UUID> sentOneWayIds = new HashSet<>();
+        if (type == SessionType.ASYNC) {
+            rpcPendingMap.entrySet().forEach(processPendingRpc(context, sessionId, server, sentOneWayIds));
+        } else {
+            rpcPendingMap.entrySet().stream().findFirst().ifPresent(processPendingRpc(context, sessionId, server, sentOneWayIds));
+        }
+
+        sentOneWayIds.forEach(rpcPendingMap::remove);
+    }
+
+    private Consumer<Map.Entry<Integer, ToDeviceRpcRequestMetadata>> processPendingRpc(ActorContext context, SessionId sessionId, Optional<ServerAddress> server, Set<UUID> sentOneWayIds) {
+        return entry -> {
+            ToDeviceRpcRequest request = entry.getValue().getMsg().getMsg();
+            ToDeviceRpcRequestBody body = request.getBody();
+            if (request.isOneway()) {
+                sentOneWayIds.add(request.getId());
+                ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(entry.getValue().getMsg(), (String) null);
+                context.parent().tell(responsePluginMsg, ActorRef.noSender());
+            }
+            ToDeviceRpcRequestMsg rpcRequest = new ToDeviceRpcRequestMsg(
+                    entry.getKey(),
+                    body.getMethod(),
+                    body.getParams()
+            );
+            ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(rpcRequest, sessionId);
+            sendMsgToSessionActor(response, server);
+        };
+    }
+
+    void process(ActorContext context, ToDeviceActorMsg msg) {
+        processSubscriptionCommands(context, msg);
+        processRpcResponses(context, msg);
+        processSessionStateMsgs(msg);
+    }
+
+    void processAttributesUpdate(ActorContext context, DeviceAttributesEventNotificationMsg msg) {
+        //TODO: improve this procedure to fetch only changed attributes.
+        refreshAttributes();
+        //TODO: support attributes deletion
+        Set<AttributeKey> keys = msg.getKeys();
+        if (attributeSubscriptions.size() > 0) {
+            ToDeviceMsg notification = null;
+            if (msg.isDeleted()) {
+                List<AttributeKey> sharedKeys = keys.stream()
+                        .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
+                        .collect(Collectors.toList());
+                notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromDeleted(sharedKeys));
+            } else {
+                List<AttributeKvEntry> attributes = keys.stream()
+                        .filter(key -> DataConstants.SHARED_SCOPE.equals(key.getScope()))
+                        .map(key -> deviceAttributes.getServerPublicAttribute(key.getAttributeKey()))
+                        .filter(Optional::isPresent)
+                        .map(Optional::get)
+                        .collect(Collectors.toList());
+                if (attributes.size() > 0) {
+                    notification = new AttributesUpdateNotification(BasicAttributeKVMsg.fromShared(attributes));
+                } else {
+                    logger.debug("[{}] No public server side attributes changed!", deviceId);
+                }
+            }
+            if (notification != null) {
+                ToDeviceMsg finalNotification = notification;
+                attributeSubscriptions.entrySet().forEach(sub -> {
+                    ToDeviceSessionActorMsg response = new BasicToDeviceSessionActorMsg(finalNotification, sub.getKey());
+                    sendMsgToSessionActor(response, sub.getValue().getServer());
+                });
+            }
+        } else {
+            logger.debug("[{}] No registered attributes subscriptions to process!", deviceId);
+        }
+    }
+
+    void process(ActorContext context, RuleChainDeviceMsg srcMsg) {
+        ChainProcessingMetaData md = new ChainProcessingMetaData(srcMsg.getRuleChain(),
+                srcMsg.getToDeviceActorMsg(), deviceAttributes, context.self());
+        ChainProcessingContext ctx = new ChainProcessingContext(md);
+        if (ctx.getChainLength() > 0) {
+            RuleProcessingMsg msg = new RuleProcessingMsg(ctx);
+            ActorRef ruleActorRef = ctx.getCurrentActor();
+            ruleActorRef.tell(msg, ActorRef.noSender());
+        } else {
+            context.self().tell(new RulesProcessedMsg(ctx), context.self());
+        }
+    }
+
+    void processRpcResponses(ActorContext context, ToDeviceActorMsg msg) {
+        SessionId sessionId = msg.getSessionId();
+        FromDeviceMsg inMsg = msg.getPayload();
+        if (inMsg.getMsgType() == MsgType.TO_DEVICE_RPC_RESPONSE) {
+            logger.debug("[{}] Processing rpc command response [{}]", deviceId, sessionId);
+            ToDeviceRpcResponseMsg responseMsg = (ToDeviceRpcResponseMsg) inMsg;
+            ToDeviceRpcRequestMetadata requestMd = rpcPendingMap.remove(responseMsg.getRequestId());
+            boolean success = requestMd != null;
+            if (success) {
+                ToPluginRpcResponseDeviceMsg responsePluginMsg = toPluginRpcResponseMsg(requestMd.getMsg(), responseMsg.getData());
+                Optional<ServerAddress> pluginServerAddress = requestMd.getMsg().getServerAddress();
+                if (pluginServerAddress.isPresent()) {
+                    systemContext.getRpcService().tell(pluginServerAddress.get(), responsePluginMsg);
+                    logger.debug("[{}] Rpc command response sent to remote plugin actor [{}]!", deviceId, requestMd.getMsg().getMsg().getId());
+                } else {
+                    context.parent().tell(responsePluginMsg, ActorRef.noSender());
+                    logger.debug("[{}] Rpc command response sent to local plugin actor [{}]!", deviceId, requestMd.getMsg().getMsg().getId());
+                }
+            } else {
+                logger.debug("[{}] Rpc command response [{}] is stale!", deviceId, responseMsg.getRequestId());
+            }
+            if (msg.getSessionType() == SessionType.SYNC) {
+                BasicCommandAckResponse response = success
+                        ? BasicCommandAckResponse.onSuccess(MsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId())
+                        : BasicCommandAckResponse.onError(MsgType.TO_DEVICE_RPC_REQUEST, responseMsg.getRequestId(), new TimeoutException());
+                sendMsgToSessionActor(new BasicToDeviceSessionActorMsg(response, msg.getSessionId()), msg.getServerAddress());
+            }
+        }
+    }
+
+    public void processClusterEventMsg(ClusterEventMsg msg) {
+        if (!msg.isAdded()) {
+            logger.debug("[{}] Clearing attributes/rpc subscription for server [{}]", deviceId, msg.getServerAddress());
+            Predicate<Map.Entry<SessionId, SessionInfo>> filter = e -> e.getValue().getServer()
+                .map(serverAddress -> serverAddress.equals(msg.getServerAddress())).orElse(false);
+            attributeSubscriptions.entrySet().removeIf(filter);
+            rpcSubscriptions.entrySet().removeIf(filter);
+        }
+    }
+
+    private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, String data) {
+        return toPluginRpcResponseMsg(requestMsg, data, null);
+    }
+
+    private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, RpcError error) {
+        return toPluginRpcResponseMsg(requestMsg, null, error);
+    }
+
+    private ToPluginRpcResponseDeviceMsg toPluginRpcResponseMsg(ToDeviceRpcRequestPluginMsg requestMsg, String data, RpcError error) {
+        return new ToPluginRpcResponseDeviceMsg(
+                requestMsg.getPluginId(),
+                requestMsg.getPluginTenantId(),
+                new FromDeviceRpcResponse(requestMsg.getMsg().getId(),
+                        data,
+                        error
+                )
+        );
+    }
+
+    void onRulesProcessedMsg(ActorContext context, RulesProcessedMsg msg) {
+        ChainProcessingContext ctx = msg.getCtx();
+        ToDeviceActorMsg inMsg = ctx.getInMsg();
+        SessionId sid = inMsg.getSessionId();
+        ToDeviceSessionActorMsg response;
+        if (ctx.getResponse() != null) {
+            response = new BasicToDeviceSessionActorMsg(ctx.getResponse(), sid);
+        } else {
+            response = new BasicToDeviceSessionActorMsg(ctx.getError(), sid);
+        }
+        sendMsgToSessionActor(response, inMsg.getServerAddress());
+    }
+
+    private void processSubscriptionCommands(ActorContext context, ToDeviceActorMsg msg) {
+        SessionId sessionId = msg.getSessionId();
+        SessionType sessionType = msg.getSessionType();
+        FromDeviceMsg inMsg = msg.getPayload();
+        if (inMsg.getMsgType() == MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST) {
+            logger.debug("[{}] Registering attributes subscription for session [{}]", deviceId, sessionId);
+            attributeSubscriptions.put(sessionId, new SessionInfo(sessionType, msg.getServerAddress()));
+        } else if (inMsg.getMsgType() == MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST) {
+            logger.debug("[{}] Canceling attributes subscription for session [{}]", deviceId, sessionId);
+            attributeSubscriptions.remove(sessionId);
+        } else if (inMsg.getMsgType() == MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST) {
+            logger.debug("[{}] Registering rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
+            rpcSubscriptions.put(sessionId, new SessionInfo(sessionType, msg.getServerAddress()));
+            sendPendingRequests(context, sessionId, sessionType, msg.getServerAddress());
+        } else if (inMsg.getMsgType() == MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST) {
+            logger.debug("[{}] Canceling rpc subscription for session [{}][{}]", deviceId, sessionId, sessionType);
+            rpcSubscriptions.remove(sessionId);
+        }
+    }
+
+    private void processSessionStateMsgs(ToDeviceActorMsg msg) {
+        SessionId sessionId = msg.getSessionId();
+        FromDeviceMsg inMsg = msg.getPayload();
+        if (inMsg instanceof SessionCloseMsg) {
+            logger.debug("[{}] Canceling subscriptions for closed session [{}]", deviceId, sessionId);
+            attributeSubscriptions.remove(sessionId);
+            rpcSubscriptions.remove(sessionId);
+        }
+    }
+
+    private void sendMsgToSessionActor(ToDeviceSessionActorMsg response, Optional<ServerAddress> sessionAddress) {
+        if (sessionAddress.isPresent()) {
+            ServerAddress address = sessionAddress.get();
+            logger.debug("{} Forwarding msg: {}", address, response);
+            systemContext.getRpcService().tell(sessionAddress.get(), response);
+        } else {
+            systemContext.getSessionManagerActor().tell(response, ActorRef.noSender());
+        }
+    }
+
+    private List<AttributeKvEntry> fetchAttributes(String attributeType) {
+        return systemContext.getAttributesService().findAll(this.deviceId, attributeType);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
new file mode 100644
index 0000000..178c6e7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/SessionInfo.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.device;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.SessionType;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class SessionInfo {
+    private final SessionType type;
+    private final Optional<ServerAddress> server;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
new file mode 100644
index 0000000..338c7eb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/device/ToDeviceRpcRequestMetadata.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.device;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequestMetadata {
+    private final ToDeviceRpcRequestPluginMsg msg;
+    private final boolean sent;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
new file mode 100644
index 0000000..1deb031
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActor.java
@@ -0,0 +1,151 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+public class PluginActor extends ComponentActor<PluginId, PluginActorMessageProcessor> {
+
+    private PluginActor(ActorSystemContext systemContext, TenantId tenantId, PluginId pluginId) {
+        super(systemContext, tenantId, pluginId);
+        setProcessor(new PluginActorMessageProcessor(tenantId, pluginId, systemContext,
+                logger, context().parent(), context().self()));
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (msg instanceof PluginWebsocketMsg) {
+            onWebsocketMsg((PluginWebsocketMsg<?>) msg);
+        } else if (msg instanceof PluginRestMsg) {
+            onRestMsg((PluginRestMsg) msg);
+        } else if (msg instanceof PluginCallbackMessage) {
+            onPluginCallback((PluginCallbackMessage) msg);
+        } else if (msg instanceof RuleToPluginMsgWrapper) {
+            onRuleToPluginMsg((RuleToPluginMsgWrapper) msg);
+        } else if (msg instanceof PluginRpcMsg) {
+            onRpcMsg((PluginRpcMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            onClusterEventMsg((ClusterEventMsg) msg);
+        } else if (msg instanceof ComponentLifecycleMsg) {
+            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+        } else if (msg instanceof ToPluginRpcResponseDeviceMsg) {
+            onRpcResponse((ToPluginRpcResponseDeviceMsg) msg);
+        } else if (msg instanceof PluginTerminationMsg) {
+            logger.info("[{}][{}] Going to terminate plugin actor.", tenantId, id);
+            context().parent().tell(msg, ActorRef.noSender());
+            context().stop(self());
+        } else if (msg instanceof TimeoutMsg) {
+            onTimeoutMsg(context(), (TimeoutMsg) msg);
+        } else if (msg instanceof StatsPersistTick) {
+            onStatsPersistTick(id);
+        } else {
+            logger.debug("[{}][{}] Unknown msg type.", tenantId, id, msg.getClass().getName());
+        }
+    }
+
+    private void onPluginCallback(PluginCallbackMessage msg) {
+        try {
+            processor.onPluginCallbackMsg(msg);
+        } catch (Exception e) {
+            logAndPersist("onPluginCallbackMsg", e);
+        }
+    }
+
+    private void onTimeoutMsg(ActorContext context, TimeoutMsg msg) {
+        processor.onTimeoutMsg(context, msg);
+    }
+
+    private void onRpcResponse(ToPluginRpcResponseDeviceMsg msg) {
+        processor.onDeviceRpcMsg(msg.getResponse());
+    }
+
+    private void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
+        logger.debug("[{}] Going to process rule msg: {}", id, msg.getMsg());
+        try {
+            processor.onRuleToPluginMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onRuleMsg", e);
+        }
+    }
+
+    private void onWebsocketMsg(PluginWebsocketMsg<?> msg) {
+        logger.debug("[{}] Going to process web socket msg: {}", id, msg);
+        try {
+            processor.onWebsocketMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onWebsocketMsg", e);
+        }
+    }
+
+    private void onRestMsg(PluginRestMsg msg) {
+        logger.debug("[{}] Going to process rest msg: {}", id, msg);
+        try {
+            processor.onRestMsg(msg);
+            increaseMessagesProcessedCount();
+        } catch (Exception e) {
+            logAndPersist("onRestMsg", e);
+        }
+    }
+
+    private void onRpcMsg(PluginRpcMsg msg) {
+        try {
+            logger.debug("[{}] Going to process rpc msg: {}", id, msg);
+            processor.onRpcMsg(msg);
+        } catch (Exception e) {
+            logAndPersist("onRpcMsg", e);
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<PluginActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final PluginId pluginId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, PluginId pluginId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.pluginId = pluginId;
+        }
+
+        @Override
+        public PluginActor create() throws Exception {
+            return new PluginActor(context, tenantId, pluginId);
+        }
+    }
+
+    @Override
+    protected long getErrorPersistFrequency() {
+        return systemContext.getPluginErrorPersistFrequency();
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
new file mode 100644
index 0000000..72ae4bb
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginActorMessageProcessor.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.Plugin;
+import org.thingsboard.server.extensions.api.plugins.PluginInitializationException;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class PluginActorMessageProcessor extends ComponentMsgProcessor<PluginId> {
+
+    private final SharedPluginProcessingContext pluginCtx;
+    private final PluginProcessingContext trustedCtx;
+    private PluginMetaData pluginMd;
+    private Plugin pluginImpl;
+    private ComponentLifecycleState state;
+
+
+    protected PluginActorMessageProcessor(TenantId tenantId, PluginId pluginId, ActorSystemContext systemContext
+            , LoggingAdapter logger, ActorRef parent, ActorRef self) {
+        super(systemContext, logger, tenantId, pluginId);
+        this.pluginCtx = new SharedPluginProcessingContext(systemContext, tenantId, pluginId, parent, self);
+        this.trustedCtx = new PluginProcessingContext(pluginCtx, null);
+    }
+
+    @Override
+    public void start() throws Exception {
+        logger.info("[{}] Going to start plugin actor.", entityId);
+        pluginMd = systemContext.getPluginService().findPluginById(entityId);
+        if (pluginMd == null) {
+            throw new PluginInitializationException("Plugin not found!");
+        }
+        if (pluginMd.getConfiguration() == null) {
+            throw new PluginInitializationException("Plugin metadata is empty!");
+        }
+        state = pluginMd.getState();
+        if (state == ComponentLifecycleState.ACTIVE) {
+            logger.info("[{}] Plugin is active. Going to initialize plugin.", entityId);
+            initComponent();
+        } else {
+            logger.info("[{}] Plugin is suspended. Skipping plugin initialization.", entityId);
+        }
+    }
+
+    @Override
+    public void stop() throws Exception {
+        onStop();
+    }
+
+    private void initComponent() {
+        try {
+            pluginImpl = initComponent(pluginMd.getClazz(), ComponentType.PLUGIN, mapper.writeValueAsString(pluginMd.getConfiguration()));
+        } catch (InstantiationException e) {
+            throw new PluginInitializationException("No default constructor for plugin implementation!", e);
+        } catch (IllegalAccessException e) {
+            throw new PluginInitializationException("Illegal Access Exception during plugin initialization!", e);
+        } catch (ClassNotFoundException e) {
+            throw new PluginInitializationException("Plugin Class not found!", e);
+        } catch (JsonProcessingException e) {
+            throw new PluginInitializationException("Plugin Configuration is invalid!", e);
+        } catch (Exception e) {
+            throw new PluginInitializationException(e.getMessage(), e);
+        }
+    }
+
+    public void onRuleToPluginMsg(RuleToPluginMsgWrapper msg) throws RuleException {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            pluginImpl.process(trustedCtx, msg.getRuleTenantId(), msg.getRuleId(), msg.getMsg());
+        } else {
+            //TODO: reply with plugin suspended message
+        }
+    }
+
+    public void onWebsocketMsg(PluginWebsocketMsg<?> msg) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            pluginImpl.process(new PluginProcessingContext(pluginCtx, msg.getSecurityCtx()), msg);
+        } else {
+            //TODO: reply with plugin suspended message
+        }
+    }
+
+    public void onRestMsg(PluginRestMsg msg) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            pluginImpl.process(new PluginProcessingContext(pluginCtx, msg.getSecurityCtx()), msg);
+        }
+    }
+
+    public void onRpcMsg(PluginRpcMsg msg) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            pluginImpl.process(trustedCtx, msg.getRpcMsg());
+        } else {
+            //TODO: reply with plugin suspended message
+        }
+    }
+
+    public void onPluginCallbackMsg(PluginCallbackMessage msg) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            if (msg.isSuccess()) {
+                msg.getCallback().onSuccess(trustedCtx, msg.getV());
+            } else {
+                msg.getCallback().onFailure(trustedCtx, msg.getE());
+            }
+        } else {
+            //TODO: reply with plugin suspended message
+        }
+    }
+
+
+    public void onTimeoutMsg(ActorContext context, TimeoutMsg<?> msg) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            pluginImpl.process(trustedCtx, msg);
+        }
+    }
+
+
+    public void onDeviceRpcMsg(FromDeviceRpcResponse response) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            pluginImpl.process(trustedCtx, response);
+        }
+    }
+
+    @Override
+    public void onClusterEventMsg(ClusterEventMsg msg) {
+        if (state == ComponentLifecycleState.ACTIVE) {
+            ServerAddress address = msg.getServerAddress();
+            if (msg.isAdded()) {
+                logger.debug("[{}] Going to process server add msg: {}", entityId, address);
+                pluginImpl.onServerAdded(trustedCtx, address);
+            } else {
+                logger.debug("[{}] Going to process server remove msg: {}", entityId, address);
+                pluginImpl.onServerRemoved(trustedCtx, address);
+            }
+        }
+    }
+
+    @Override
+    public void onCreated(ActorContext context) {
+        logger.info("[{}] Going to process onCreated plugin.", entityId);
+    }
+
+    @Override
+    public void onUpdate(ActorContext context) throws Exception {
+        PluginMetaData oldPluginMd = systemContext.getPluginService().findPluginById(entityId);
+        pluginMd = systemContext.getPluginService().findPluginById(entityId);
+        boolean requiresRestart = false;
+        logger.info("[{}] Plugin configuration was updated from {} to {}.", entityId, oldPluginMd, pluginMd);
+        if (!oldPluginMd.getClazz().equals(pluginMd.getClazz())) {
+            logger.info("[{}] Plugin requires restart due to clazz change from {} to {}.",
+                    entityId, oldPluginMd.getClazz(), pluginMd.getClazz());
+            requiresRestart = true;
+        } else if (oldPluginMd.getConfiguration().equals(pluginMd.getConfiguration())) {
+            logger.info("[{}] Plugin requires restart due to configuration change from {} to {}.",
+                    entityId, oldPluginMd.getConfiguration(), pluginMd.getConfiguration());
+            requiresRestart = true;
+        }
+        if (requiresRestart) {
+            this.state = ComponentLifecycleState.SUSPENDED;
+            if (pluginImpl != null) {
+                pluginImpl.stop(trustedCtx);
+            }
+            start();
+        }
+    }
+
+    @Override
+    public void onStop(ActorContext context) {
+        onStop();
+        scheduleMsgWithDelay(context, new PluginTerminationMsg(entityId), systemContext.getPluginActorTerminationDelay());
+    }
+
+    private void onStop() {
+        logger.info("[{}] Going to process onStop plugin.", entityId);
+        this.state = ComponentLifecycleState.SUSPENDED;
+        if (pluginImpl != null) {
+            pluginImpl.stop(trustedCtx);
+        }
+    }
+
+    @Override
+    public void onActivate(ActorContext context) throws Exception {
+        logger.info("[{}] Going to process onActivate plugin.", entityId);
+        this.state = ComponentLifecycleState.ACTIVE;
+        if (pluginImpl != null) {
+            pluginImpl.resume(trustedCtx);
+            logger.info("[{}] Plugin resumed.", entityId);
+        } else {
+            start();
+        }
+    }
+
+    @Override
+    public void onSuspend(ActorContext context) {
+        logger.info("[{}] Going to process onSuspend plugin.", entityId);
+        this.state = ComponentLifecycleState.SUSPENDED;
+        if (pluginImpl != null) {
+            pluginImpl.suspend(trustedCtx);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java
new file mode 100644
index 0000000..193ff31
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginCallbackMessage.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public final class PluginCallbackMessage<V> {
+    @Getter
+    private final PluginCallback<V> callback;
+    @Getter
+    private final boolean success;
+    @Getter
+    private final V v;
+    @Getter
+    private final Exception e;
+
+    public static <V> PluginCallbackMessage<V> onSuccess(PluginCallback<V> callback, V data) {
+        return new PluginCallbackMessage<V>(true, callback, data, null);
+    }
+
+    public static <V> PluginCallbackMessage<V> onError(PluginCallback<V> callback, Exception e) {
+        return new PluginCallbackMessage<V>(false, callback, null, e);
+    }
+
+    private PluginCallbackMessage(boolean success, PluginCallback<V> callback, V v, Exception e) {
+        this.success = success;
+        this.callback = callback;
+        this.v = v;
+        this.e = e;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
new file mode 100644
index 0000000..5541a24
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginProcessingContext.java
@@ -0,0 +1,306 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+
+import com.datastax.driver.core.ResultSet;
+import com.datastax.driver.core.ResultSetFuture;
+import com.datastax.driver.core.Row;
+import com.google.common.base.Function;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+import akka.actor.ActorRef;
+
+import javax.annotation.Nullable;
+
+@Slf4j
+public final class PluginProcessingContext implements PluginContext {
+
+    private static final Executor executor = Executors.newSingleThreadExecutor();
+
+    private final SharedPluginProcessingContext pluginCtx;
+    private final Optional<PluginApiCallSecurityContext> securityCtx;
+
+    public PluginProcessingContext(SharedPluginProcessingContext pluginCtx, PluginApiCallSecurityContext securityCtx) {
+        super();
+        this.pluginCtx = pluginCtx;
+        this.securityCtx = Optional.ofNullable(securityCtx);
+    }
+
+    @Override
+    public void sendPluginRpcMsg(RpcMsg msg) {
+        this.pluginCtx.rpcService.tell(new PluginRpcMsg(pluginCtx.tenantId, pluginCtx.pluginId, msg));
+    }
+
+    @Override
+    public void send(PluginWebsocketMsg<?> wsMsg) throws IOException {
+        pluginCtx.msgEndpoint.send(wsMsg);
+    }
+
+    @Override
+    public void close(PluginWebsocketSessionRef sessionRef) throws IOException {
+        pluginCtx.msgEndpoint.close(sessionRef);
+    }
+
+    @Override
+    public void saveAttributes(DeviceId deviceId, String scope, List<AttributeKvEntry> attributes, PluginCallback<Void> callback) {
+        validate(deviceId);
+        Set<AttributeKey> keys = new HashSet<>();
+        for (AttributeKvEntry attribute : attributes) {
+            keys.add(new AttributeKey(scope, attribute.getKey()));
+        }
+
+        ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.attributesService.save(deviceId, scope, attributes);
+        Futures.addCallback(rsListFuture, getListCallback(callback, v -> {
+            onDeviceAttributesChanged(deviceId, keys);
+            return null;
+        }), executor);
+    }
+
+    @Override
+    public Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey) {
+        validate(deviceId);
+        AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
+        return Optional.ofNullable(attribute);
+    }
+
+    @Override
+    public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys) {
+        validate(deviceId);
+        List<AttributeKvEntry> result = new ArrayList<>(attributeKeys.size());
+        for (String attributeKey : attributeKeys) {
+            AttributeKvEntry attribute = pluginCtx.attributesService.find(deviceId, attributeType, attributeKey);
+            if (attribute != null) {
+                result.add(attribute);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType) {
+        validate(deviceId);
+        return pluginCtx.attributesService.findAll(deviceId, attributeType);
+    }
+
+    @Override
+    public void removeAttributes(DeviceId deviceId, String scope, List<String> keys) {
+        validate(deviceId);
+        pluginCtx.attributesService.removeAll(deviceId, scope, keys);
+        onDeviceAttributesDeleted(deviceId, keys.stream().map(key -> new AttributeKey(scope, key)).collect(Collectors.toSet()));
+    }
+
+    @Override
+    public void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback) {
+        validate(deviceId);
+        ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entry);
+        Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
+    }
+
+    @Override
+    public void saveTsData(DeviceId deviceId, List<TsKvEntry> entries, PluginCallback<Void> callback) {
+        validate(deviceId);
+        ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.save(DataConstants.DEVICE, deviceId, entries);
+        Futures.addCallback(rsListFuture, getListCallback(callback, v -> null), executor);
+    }
+
+    @Override
+    public List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query) {
+        validate(deviceId);
+        return pluginCtx.tsService.find(DataConstants.DEVICE, deviceId, query);
+    }
+
+    @Override
+    public void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback) {
+        validate(deviceId);
+        ResultSetFuture future = pluginCtx.tsService.findAllLatest(DataConstants.DEVICE, deviceId);
+        Futures.addCallback(future, getCallback(callback, pluginCtx.tsService::convertResultSetToTsKvEntryList), executor);
+    }
+
+    @Override
+    public void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback) {
+        validate(deviceId);
+        ListenableFuture<List<ResultSet>> rsListFuture = pluginCtx.tsService.findLatest(DataConstants.DEVICE, deviceId, keys);
+        Futures.addCallback(rsListFuture, getListCallback(callback, rsList ->
+        {
+            List<TsKvEntry> result = new ArrayList<>();
+            for (ResultSet rs : rsList) {
+                Row row = rs.one();
+                if (row != null) {
+                    result.add(pluginCtx.tsService.convertResultToTsKvEntry(row));
+                }
+            }
+            return result;
+        }), executor);
+    }
+
+    @Override
+    public void reply(PluginToRuleMsg<?> msg) {
+        pluginCtx.parentActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public boolean checkAccess(DeviceId deviceId) {
+        try {
+            return validate(deviceId);
+        } catch (IllegalStateException | IllegalArgumentException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public PluginId getPluginId() {
+        return pluginCtx.pluginId;
+    }
+
+    @Override
+    public Optional<PluginApiCallSecurityContext> getSecurityCtx() {
+        return securityCtx;
+    }
+
+    private void onDeviceAttributesChanged(DeviceId deviceId, AttributeKey key) {
+        onDeviceAttributesChanged(deviceId, Collections.singleton(key));
+    }
+
+    private void onDeviceAttributesDeleted(DeviceId deviceId, Set<AttributeKey> keys) {
+        Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+        pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onDelete(device.getTenantId(), deviceId, keys));
+    }
+
+    private void onDeviceAttributesChanged(DeviceId deviceId, Set<AttributeKey> keys) {
+        Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+        pluginCtx.toDeviceActor(DeviceAttributesEventNotificationMsg.onUpdate(device.getTenantId(), deviceId, keys));
+    }
+
+    private <T> FutureCallback<List<ResultSet>> getListCallback(final PluginCallback<T> callback, Function<List<ResultSet>, T> transformer) {
+        return new FutureCallback<List<ResultSet>>() {
+            @Override
+            public void onSuccess(@Nullable List<ResultSet> result) {
+                pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender());
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                if (t instanceof Exception) {
+                    pluginCtx.self().tell(PluginCallbackMessage.onError(callback, (Exception) t), ActorRef.noSender());
+                } else {
+                    log.error("Critical error: {}", t.getMessage(), t);
+                }
+            }
+        };
+    }
+
+    private <T> FutureCallback<ResultSet> getCallback(final PluginCallback<T> callback, Function<ResultSet, T> transformer) {
+        return new FutureCallback<ResultSet>() {
+            @Override
+            public void onSuccess(@Nullable ResultSet result) {
+                pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, transformer.apply(result)), ActorRef.noSender());
+            }
+
+            @Override
+            public void onFailure(Throwable t) {
+                if (t instanceof Exception) {
+                    pluginCtx.self().tell(PluginCallbackMessage.onError(callback, (Exception) t), ActorRef.noSender());
+                } else {
+                    log.error("Critical error: {}", t.getMessage(), t);
+                }
+            }
+        };
+    }
+
+    // TODO: replace with our own exceptions
+    private boolean validate(DeviceId deviceId) {
+        if (securityCtx.isPresent()) {
+            PluginApiCallSecurityContext ctx = securityCtx.get();
+            if (ctx.isTenantAdmin() || ctx.isCustomerUser()) {
+                Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+                if (device == null) {
+                    throw new IllegalStateException("Device not found!");
+                } else {
+                    if (!device.getTenantId().equals(ctx.getTenantId())) {
+                        throw new IllegalArgumentException("Device belongs to different tenant!");
+                    } else if (ctx.isCustomerUser() && !device.getCustomerId().equals(ctx.getCustomerId())) {
+                        throw new IllegalArgumentException("Device belongs to different customer!");
+                    }
+                }
+            } else {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public Optional<ServerAddress> resolve(DeviceId deviceId) {
+        return pluginCtx.routingService.resolve(deviceId);
+    }
+
+    @Override
+    public void getDevice(DeviceId deviceId, PluginCallback<Device> callback) {
+        //TODO: add caching here with async api.
+        Device device = pluginCtx.deviceService.findDeviceById(deviceId);
+        pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, device), ActorRef.noSender());
+    }
+
+    @Override
+    public void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback) {
+        //TODO: add caching here with async api.
+        List<Device> devices = pluginCtx.deviceService.findDevicesByTenantIdAndCustomerId(tenantId, customerId, new TextPageLink(limit)).getData();
+        pluginCtx.self().tell(PluginCallbackMessage.onSuccess(callback, devices), ActorRef.noSender());
+    }
+
+    @Override
+    public void sendRpcRequest(ToDeviceRpcRequest msg) {
+        pluginCtx.sendRpcRequest(msg);
+    }
+
+    @Override
+    public void scheduleTimeoutMsg(TimeoutMsg msg) {
+        pluginCtx.scheduleTimeoutMsg(msg);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/PluginTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginTerminationMsg.java
new file mode 100644
index 0000000..4363109
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/PluginTerminationMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import org.thingsboard.server.actors.shared.ActorTerminationMsg;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.SessionId;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class PluginTerminationMsg extends ActorTerminationMsg<PluginId> {
+
+    public PluginTerminationMsg(PluginId id) {
+        super(id);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java b/application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java
new file mode 100644
index 0000000..599ad68
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/RuleToPluginMsgWrapper.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.aware.RuleAwareMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+
+public class RuleToPluginMsgWrapper implements ToPluginActorMsg, RuleAwareMsg {
+
+    private final TenantId pluginTenantId;
+    private final PluginId pluginId;
+    private final TenantId ruleTenantId;
+    private final RuleId ruleId;
+    private final RuleToPluginMsg<?> msg;
+
+    public RuleToPluginMsgWrapper(TenantId pluginTenantId, PluginId pluginId, TenantId ruleTenantId, RuleId ruleId, RuleToPluginMsg<?> msg) {
+        super();
+        this.pluginTenantId = pluginTenantId;
+        this.pluginId = pluginId;
+        this.ruleTenantId = ruleTenantId;
+        this.ruleId = ruleId;
+        this.msg = msg;
+    }
+
+    @Override
+    public TenantId getPluginTenantId() {
+        return pluginTenantId;
+    }
+
+    @Override
+    public PluginId getPluginId() {
+        return pluginId;
+    }
+
+    public TenantId getRuleTenantId() {
+        return ruleTenantId;
+    }
+
+    @Override
+    public RuleId getRuleId() {
+        return ruleId;
+    }
+
+
+    public RuleToPluginMsg<?> getMsg() {
+        return msg;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
new file mode 100644
index 0000000..71a6ac9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/SharedPluginProcessingContext.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import akka.actor.ActorRef;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.controller.plugin.PluginWebSocketMsgEndpoint;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.api.device.DeviceAttributesEventNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import scala.concurrent.duration.Duration;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+import java.util.function.BiConsumer;
+
+@Slf4j
+public final class SharedPluginProcessingContext {
+    final ActorRef parentActor;
+    final ActorRef currentActor;
+    final ActorSystemContext systemContext;
+    final PluginWebSocketMsgEndpoint msgEndpoint;
+    final DeviceService deviceService;
+    final TimeseriesService tsService;
+    final AttributesService attributesService;
+    final ClusterRpcService rpcService;
+    final ClusterRoutingService routingService;
+    final PluginId pluginId;
+    final TenantId tenantId;
+
+    public SharedPluginProcessingContext(ActorSystemContext sysContext, TenantId tenantId, PluginId pluginId,
+                                         ActorRef parentActor, ActorRef self) {
+        super();
+        this.tenantId = tenantId;
+        this.pluginId = pluginId;
+        this.parentActor = parentActor;
+        this.currentActor = self;
+        this.systemContext = sysContext;
+        this.msgEndpoint = sysContext.getWsMsgEndpoint();
+        this.tsService = sysContext.getTsService();
+        this.attributesService = sysContext.getAttributesService();
+        this.deviceService = sysContext.getDeviceService();
+        this.rpcService = sysContext.getRpcService();
+        this.routingService = sysContext.getRoutingService();
+    }
+
+    public PluginId getPluginId() {
+        return pluginId;
+    }
+
+    public void toDeviceActor(DeviceAttributesEventNotificationMsg msg) {
+        forward(msg.getDeviceId(), msg, rpcService::tell);
+    }
+
+    public void sendRpcRequest(ToDeviceRpcRequest msg) {
+        log.trace("[{}] Forwarding msg {} to device actor!", pluginId, msg);
+        ToDeviceRpcRequestPluginMsg rpcMsg = new ToDeviceRpcRequestPluginMsg(pluginId, tenantId, msg);
+        forward(msg.getDeviceId(), rpcMsg, rpcService::tell);
+    }
+
+    private <T> void forward(DeviceId deviceId, T msg, BiConsumer<ServerAddress, T> rpcFunction) {
+        Optional<ServerAddress> instance = routingService.resolve(deviceId);
+        if (instance.isPresent()) {
+            log.trace("[{}] Forwarding msg {} to remote device actor!", pluginId, msg);
+            rpcFunction.accept(instance.get(), msg);
+        } else {
+            log.trace("[{}] Forwarding msg {} to local device actor!", pluginId, msg);
+            parentActor.tell(msg, ActorRef.noSender());
+        }
+    }
+
+    public void scheduleTimeoutMsg(TimeoutMsg msg) {
+        log.debug("Scheduling msg {} with delay {} ms", msg, msg.getTimeout());
+        systemContext.getScheduler().scheduleOnce(
+                Duration.create(msg.getTimeout(), TimeUnit.MILLISECONDS),
+                currentActor,
+                msg,
+                systemContext.getActorSystem().dispatcher(),
+                ActorRef.noSender());
+
+    }
+
+    public ActorRef self() {
+        return currentActor;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/plugin/TimeoutScheduler.java b/application/src/main/java/org/thingsboard/server/actors/plugin/TimeoutScheduler.java
new file mode 100644
index 0000000..44a4676
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/plugin/TimeoutScheduler.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.plugin;
+
+import akka.actor.ActorRef;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface TimeoutScheduler {
+
+    void scheduleMsgWithDelay(Object msg, long delayInMs);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
new file mode 100644
index 0000000..64a9a73
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/BasicRpcSessionListener.java
@@ -0,0 +1,170 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import akka.actor.ActorRef;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.SerializationUtils;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.service.cluster.rpc.GrpcSession;
+import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class BasicRpcSessionListener implements GrpcSessionListener {
+
+    private final ActorSystemContext context;
+    private final ActorService service;
+    private final ActorRef manager;
+    private final ActorRef self;
+
+    public BasicRpcSessionListener(ActorSystemContext context, ActorRef manager, ActorRef self) {
+        this.context = context;
+        this.service = context.getActorService();
+        this.manager = manager;
+        this.self = self;
+    }
+
+    @Override
+    public void onConnected(GrpcSession session) {
+        log.info("{} session started -> {}", getType(session), session.getRemoteServer());
+        if (!session.isClient()) {
+            manager.tell(new RpcSessionConnectedMsg(session.getRemoteServer(), session.getSessionId()), self);
+        }
+    }
+
+    @Override
+    public void onDisconnected(GrpcSession session) {
+        log.info("{} session closed -> {}", getType(session), session.getRemoteServer());
+        manager.tell(new RpcSessionDisconnectedMsg(session.isClient(), session.getRemoteServer()), self);
+    }
+
+    @Override
+    public void onToPluginRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcMessage msg) {
+        if (log.isTraceEnabled()) {
+            log.trace("{} session [{}] received plugin msg {}", getType(session), session.getRemoteServer(), msg);
+        }
+        service.onMsg(convert(session.getRemoteServer(), msg));
+    }
+
+    @Override
+    public void onToDeviceActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorRpcMessage msg) {
+        log.trace("{} session [{}] received device actor msg {}", getType(session), session.getRemoteServer(), msg);
+        service.onMsg((ToDeviceActorMsg) deserialize(msg.getData().toByteArray()));
+    }
+
+    @Override
+    public void onToDeviceActorNotificationRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorNotificationRpcMessage msg) {
+        log.trace("{} session [{}] received device actor notification msg {}", getType(session), session.getRemoteServer(), msg);
+        service.onMsg((ToDeviceActorNotificationMsg) deserialize(msg.getData().toByteArray()));
+    }
+
+    @Override
+    public void onToDeviceSessionActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceSessionActorRpcMessage msg) {
+        log.trace("{} session [{}] received session actor msg {}", getType(session), session.getRemoteServer(), msg);
+        service.onMsg((ToDeviceSessionActorMsg) deserialize(msg.getData().toByteArray()));
+    }
+
+    @Override
+    public void onToDeviceRpcRequestRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage msg) {
+        log.trace("{} session [{}] received session actor msg {}", getType(session), session.getRemoteServer(), msg);
+        service.onMsg(deserialize(session.getRemoteServer(), msg));
+    }
+
+    @Override
+    public void onFromDeviceRpcResponseRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcResponseRpcMessage msg) {
+        log.trace("{} session [{}] received session actor msg {}", getType(session), session.getRemoteServer(), msg);
+        service.onMsg(deserialize(session.getRemoteServer(), msg));
+    }
+
+    @Override
+    public void onToAllNodesRpcMessage(GrpcSession session, ClusterAPIProtos.ToAllNodesRpcMessage msg) {
+        log.trace("{} session [{}] received session actor msg {}", getType(session), session.getRemoteServer(), msg);
+        service.onMsg((ToAllNodesMsg) deserialize(msg.getData().toByteArray()));
+    }
+
+    @Override
+    public void onError(GrpcSession session, Throwable t) {
+        log.warn("{} session got error -> {}", getType(session), session.getRemoteServer(), t);
+        manager.tell(new RpcSessionClosedMsg(session.isClient(), session.getRemoteServer()), self);
+        session.close();
+    }
+
+    private static String getType(GrpcSession session) {
+        return session.isClient() ? "Client" : "Server";
+    }
+
+    private static PluginRpcMsg convert(ServerAddress serverAddress, ClusterAPIProtos.ToPluginRpcMessage msg) {
+        ClusterAPIProtos.PluginAddress address = msg.getAddress();
+        TenantId tenantId = new TenantId(toUUID(address.getTenantId()));
+        PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
+        RpcMsg rpcMsg = new RpcMsg(serverAddress, msg.getClazz(), msg.getData().toByteArray());
+        return new PluginRpcMsg(tenantId, pluginId, rpcMsg);
+    }
+
+    private static UUID toUUID(ClusterAPIProtos.Uid uid) {
+        return new UUID(uid.getPluginUuidMsb(), uid.getPluginUuidLsb());
+    }
+
+    private static ToDeviceRpcRequestPluginMsg deserialize(ServerAddress serverAddress, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage msg) {
+        ClusterAPIProtos.PluginAddress address = msg.getAddress();
+        TenantId pluginTenantId = new TenantId(toUUID(address.getTenantId()));
+        PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
+
+        TenantId deviceTenantId = new TenantId(toUUID(msg.getDeviceTenantId()));
+        DeviceId deviceId = new DeviceId(toUUID(msg.getDeviceId()));
+
+        ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(msg.getMethod(), msg.getParams());
+        ToDeviceRpcRequest request = new ToDeviceRpcRequest(toUUID(msg.getMsgId()), deviceTenantId, deviceId, msg.getOneway(), msg.getExpTime(), requestBody);
+
+        return new ToDeviceRpcRequestPluginMsg(serverAddress, pluginId, pluginTenantId, request);
+    }
+
+    private static ToPluginRpcResponseDeviceMsg deserialize(ServerAddress serverAddress, ClusterAPIProtos.ToPluginRpcResponseRpcMessage msg) {
+        ClusterAPIProtos.PluginAddress address = msg.getAddress();
+        TenantId pluginTenantId = new TenantId(toUUID(address.getTenantId()));
+        PluginId pluginId = new PluginId(toUUID(address.getPluginId()));
+
+        RpcError error = !StringUtils.isEmpty(msg.getError()) ? RpcError.valueOf(msg.getError()) : null;
+        FromDeviceRpcResponse response = new FromDeviceRpcResponse(toUUID(msg.getMsgId()), msg.getResponse(), error);
+        return new ToPluginRpcResponseDeviceMsg(pluginId, pluginTenantId, response);
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <T extends Serializable> T deserialize(byte[] data) {
+        return (T) SerializationUtils.deserialize(data);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
new file mode 100644
index 0000000..a528dd6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcBroadcastMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcBroadcastMsg {
+    private final ClusterAPIProtos.ToRpcServerMessage msg;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
new file mode 100644
index 0000000..27ffdba
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcManagerActor.java
@@ -0,0 +1,192 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+
+import java.util.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcManagerActor extends ContextAwareActor {
+
+    private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
+
+    private final Map<ServerAddress, SessionActorInfo> sessionActors;
+
+    private final Map<ServerAddress, Queue<ClusterAPIProtos.ToRpcServerMessage>> pendingMsgs;
+
+    private final ServerAddress instance;
+
+    public RpcManagerActor(ActorSystemContext systemContext) {
+        super(systemContext);
+        this.sessionActors = new HashMap<>();
+        this.pendingMsgs = new HashMap<>();
+        this.instance = systemContext.getDiscoveryService().getCurrentServer().getServerAddress();
+
+        systemContext.getDiscoveryService().getOtherServers().stream()
+                .filter(otherServer -> otherServer.getServerAddress().compareTo(instance) > 0)
+                .forEach(otherServer -> onCreateSessionRequest(
+                        new RpcSessionCreateRequestMsg(UUID.randomUUID(), otherServer.getServerAddress(), null)));
+
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (msg instanceof RpcSessionTellMsg) {
+            onMsg((RpcSessionTellMsg) msg);
+        } else if (msg instanceof RpcBroadcastMsg) {
+            onMsg((RpcBroadcastMsg) msg);
+        } else if (msg instanceof RpcSessionCreateRequestMsg) {
+            onCreateSessionRequest((RpcSessionCreateRequestMsg) msg);
+        } else if (msg instanceof RpcSessionConnectedMsg) {
+            onSessionConnected((RpcSessionConnectedMsg) msg);
+        } else if (msg instanceof RpcSessionDisconnectedMsg) {
+            onSessionDisconnected((RpcSessionDisconnectedMsg) msg);
+        } else if (msg instanceof RpcSessionClosedMsg) {
+            onSessionClosed((RpcSessionClosedMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            onClusterEvent((ClusterEventMsg) msg);
+        }
+    }
+
+    private void onMsg(RpcBroadcastMsg msg) {
+        log.debug("Forwarding msg to session actors {}", msg);
+        sessionActors.keySet().forEach(address -> onMsg(new RpcSessionTellMsg(address, msg.getMsg())));
+        pendingMsgs.values().forEach(queue -> queue.add(msg.getMsg()));
+    }
+
+    private void onMsg(RpcSessionTellMsg msg) {
+        ServerAddress address = msg.getServerAddress();
+        SessionActorInfo session = sessionActors.get(address);
+        if (session != null) {
+            log.debug("{} Forwarding msg to session actor", address);
+            session.actor.tell(msg, ActorRef.noSender());
+        } else {
+            log.debug("{} Storing msg to pending queue", address);
+            Queue<ClusterAPIProtos.ToRpcServerMessage> queue = pendingMsgs.get(address);
+            if (queue == null) {
+                queue = new LinkedList<>();
+                pendingMsgs.put(address, queue);
+            }
+            queue.add(msg.getMsg());
+        }
+    }
+
+    @Override
+    public void postStop() {
+        sessionActors.clear();
+        pendingMsgs.clear();
+    }
+
+    private void onClusterEvent(ClusterEventMsg msg) {
+        ServerAddress server = msg.getServerAddress();
+        if (server.compareTo(instance) > 0) {
+            if (msg.isAdded()) {
+                onCreateSessionRequest(new RpcSessionCreateRequestMsg(UUID.randomUUID(), server, null));
+            } else {
+                onSessionClose(false, server);
+            }
+        }
+    }
+
+    private void onSessionConnected(RpcSessionConnectedMsg msg) {
+        register(msg.getRemoteAddress(), msg.getId(), context().sender());
+    }
+
+    private void onSessionDisconnected(RpcSessionDisconnectedMsg msg) {
+        boolean reconnect = msg.isClient() && isRegistered(msg.getRemoteAddress());
+        onSessionClose(reconnect, msg.getRemoteAddress());
+    }
+
+    private void onSessionClosed(RpcSessionClosedMsg msg) {
+        boolean reconnect = msg.isClient() && isRegistered(msg.getRemoteAddress());
+        onSessionClose(reconnect, msg.getRemoteAddress());
+    }
+
+    private boolean isRegistered(ServerAddress address) {
+        for (ServerInstance server : systemContext.getDiscoveryService().getOtherServers()) {
+            if (server.getServerAddress().equals(address)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private void onSessionClose(boolean reconnect, ServerAddress remoteAddress) {
+        log.debug("[{}] session closed. Should reconnect: {}", remoteAddress, reconnect);
+        SessionActorInfo sessionRef = sessionActors.get(remoteAddress);
+        if (context().sender().equals(sessionRef.actor)) {
+            sessionActors.remove(remoteAddress);
+            pendingMsgs.remove(remoteAddress);
+            if (reconnect) {
+                onCreateSessionRequest(new RpcSessionCreateRequestMsg(sessionRef.sessionId, remoteAddress, null));
+            }
+        }
+    }
+
+    private void onCreateSessionRequest(RpcSessionCreateRequestMsg msg) {
+        ActorRef actorRef = createSessionActor(msg);
+        if (msg.getRemoteAddress() != null) {
+            register(msg.getRemoteAddress(), msg.getMsgUid(), actorRef);
+        }
+    }
+
+    private void register(ServerAddress remoteAddress, UUID uuid, ActorRef sender) {
+        sessionActors.put(remoteAddress, new SessionActorInfo(uuid, sender));
+        log.debug("[{}][{}] Registering session actor.", remoteAddress, uuid);
+        Queue<ClusterAPIProtos.ToRpcServerMessage> data = pendingMsgs.remove(remoteAddress);
+        if (data != null) {
+            log.debug("[{}][{}] Forwarding {} pending messages.", remoteAddress, uuid, data.size());
+            data.forEach(msg -> sender.tell(new RpcSessionTellMsg(remoteAddress, msg), ActorRef.noSender()));
+        } else {
+            log.debug("[{}][{}] No pending messages to forward.", remoteAddress, uuid);
+        }
+    }
+
+    private ActorRef createSessionActor(RpcSessionCreateRequestMsg msg) {
+        log.debug("[{}] Creating session actor.", msg.getMsgUid());
+        ActorRef actor = context().actorOf(
+                Props.create(new RpcSessionActor.ActorCreator(systemContext, msg.getMsgUid())).withDispatcher(DefaultActorService.RPC_DISPATCHER_NAME));
+        actor.tell(msg, context().self());
+        return actor;
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RpcManagerActor> {
+        private static final long serialVersionUID = 1L;
+
+        public ActorCreator(ActorSystemContext context) {
+            super(context);
+        }
+
+        @Override
+        public RpcManagerActor create() throws Exception {
+            return new RpcManagerActor(context);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
new file mode 100644
index 0000000..a66fbc5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionActor.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import io.grpc.Channel;
+import io.grpc.ManagedChannelBuilder;
+import io.grpc.stub.StreamObserver;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
+import org.thingsboard.server.service.cluster.rpc.GrpcSession;
+import org.thingsboard.server.service.cluster.rpc.GrpcSessionListener;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcSessionActor extends ContextAwareActor {
+
+    private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
+
+    private final UUID sessionId;
+    private GrpcSession session;
+    private GrpcSessionListener listener;
+
+    public RpcSessionActor(ActorSystemContext systemContext, UUID sessionId) {
+        super(systemContext);
+        this.sessionId = sessionId;
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (msg instanceof RpcSessionTellMsg) {
+            tell((RpcSessionTellMsg) msg);
+        } else if (msg instanceof RpcSessionCreateRequestMsg) {
+            initSession((RpcSessionCreateRequestMsg) msg);
+        }
+    }
+
+    private void tell(RpcSessionTellMsg msg) {
+        session.sendMsg(msg.getMsg());
+    }
+
+    @Override
+    public void postStop() {
+        log.info("Closing session -> {}", session.getRemoteServer());
+        session.close();
+    }
+
+    private void initSession(RpcSessionCreateRequestMsg msg) {
+        log.info("[{}] Initializing session", context().self());
+        ServerAddress remoteServer = msg.getRemoteAddress();
+        listener = new BasicRpcSessionListener(systemContext, context().parent(), context().self());
+        if (msg.getRemoteAddress() == null) {
+            // Server session
+            session = new GrpcSession(listener);
+            session.setOutputStream(msg.getResponseObserver());
+            session.initInputStream();
+            session.initOutputStream();
+            systemContext.getRpcService().onSessionCreated(msg.getMsgUid(), session.getInputStream());
+        } else {
+            // Client session
+            Channel channel = ManagedChannelBuilder.forAddress(remoteServer.getHost(), remoteServer.getPort()).usePlaintext(true).build();
+            session = new GrpcSession(remoteServer, listener);
+            session.initInputStream();
+
+            ClusterRpcServiceGrpc.ClusterRpcServiceStub stub = ClusterRpcServiceGrpc.newStub(channel);
+            StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream = stub.handlePluginMsgs(session.getInputStream());
+
+            session.setOutputStream(outputStream);
+            session.initOutputStream();
+            outputStream.onNext(toConnectMsg());
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RpcSessionActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final UUID sessionId;
+
+        public ActorCreator(ActorSystemContext context, UUID sessionId) {
+            super(context);
+            this.sessionId = sessionId;
+        }
+
+        @Override
+        public RpcSessionActor create() throws Exception {
+            return new RpcSessionActor(context, sessionId);
+        }
+    }
+
+    private ClusterAPIProtos.ToRpcServerMessage toConnectMsg() {
+        ServerAddress instance = systemContext.getDiscoveryService().getCurrentServer().getServerAddress();
+        return ClusterAPIProtos.ToRpcServerMessage.newBuilder().setConnectMsg(
+                ClusterAPIProtos.ConnectRpcMessage.newBuilder().setServerAddress(
+                        ClusterAPIProtos.ServerAddress.newBuilder().setHost(instance.getHost()).setPort(instance.getPort()).build()).build()).build();
+
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionClosedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionClosedMsg.java
new file mode 100644
index 0000000..33bde07
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionClosedMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionClosedMsg {
+
+    private final boolean client;
+    private final ServerAddress remoteAddress;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionConnectedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionConnectedMsg.java
new file mode 100644
index 0000000..0cacf6c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionConnectedMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionConnectedMsg {
+
+    private final ServerAddress remoteAddress;
+    private final UUID id;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
new file mode 100644
index 0000000..0fe2817
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionCreateRequestMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import io.grpc.stub.StreamObserver;
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionCreateRequestMsg {
+
+    private final UUID msgUid;
+    private final ServerAddress remoteAddress;
+    private final StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver;
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java
new file mode 100644
index 0000000..fe6087c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionDisconnectedMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionDisconnectedMsg {
+
+    private final boolean client;
+    private final ServerAddress remoteAddress;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
new file mode 100644
index 0000000..7a1853a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/RpcSessionTellMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class RpcSessionTellMsg {
+    private final ServerAddress serverAddress;
+    private final ClusterAPIProtos.ToRpcServerMessage msg;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rpc/SessionActorInfo.java b/application/src/main/java/org/thingsboard/server/actors/rpc/SessionActorInfo.java
new file mode 100644
index 0000000..70bad39
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rpc/SessionActorInfo.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rpc;
+
+import akka.actor.ActorRef;
+import lombok.Data;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class SessionActorInfo {
+    protected final UUID sessionId;
+    protected final ActorRef actor;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java
new file mode 100644
index 0000000..2c7adef
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingContext.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import akka.actor.ActorRef;
+import org.thingsboard.server.common.msg.core.RuleEngineError;
+import org.thingsboard.server.common.msg.core.RuleEngineErrorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+
+public class ChainProcessingContext {
+
+    private final ChainProcessingMetaData md;
+    private final int index;
+    private final RuleEngineError error;
+    private ToDeviceMsg response;
+
+
+    public ChainProcessingContext(ChainProcessingMetaData md) {
+        super();
+        this.md = md;
+        this.index = 0;
+        this.error = RuleEngineError.NO_RULES;
+    }
+
+    private ChainProcessingContext(ChainProcessingContext other, int indexOffset, RuleEngineError error) {
+        super();
+        this.md = other.md;
+        this.index = other.index + indexOffset;
+        this.error = error;
+        this.response = other.response;
+
+        if (this.index < 0 || this.index >= this.md.chain.size()) {
+            throw new IllegalArgumentException("Can't apply offset " + indexOffset + " to the chain!");
+        }
+    }
+
+    public ActorRef getDeviceActor() {
+        return md.originator;
+    }
+
+    public ActorRef getCurrentActor() {
+        return md.chain.getRuleActorMd(index).getActorRef();
+    }
+
+    public boolean hasNext() {
+        return (getChainLength() - 1) > index;
+    }
+
+    public boolean isFailure() {
+        return (error != null && error.isCritical()) || (response != null && !response.isSuccess());
+    }
+
+    public ChainProcessingContext getNext() {
+        return new ChainProcessingContext(this, 1, this.error);
+    }
+
+    public ChainProcessingContext withError(RuleEngineError error) {
+        if (error != null && (this.error == null || this.error.getPriority() < error.getPriority())) {
+            return new ChainProcessingContext(this, 0, error);
+        } else {
+            return this;
+        }
+    }
+
+    public int getChainLength() {
+        return md.chain.size();
+    }
+
+    public ToDeviceActorMsg getInMsg() {
+        return md.inMsg;
+    }
+
+    public DeviceAttributes getAttributes() {
+        return md.deviceAttributes;
+    }
+
+    public ToDeviceMsg getResponse() {
+        return response;
+    }
+
+    public void mergeResponse(ToDeviceMsg response) {
+        // TODO add merge logic
+        this.response = response;
+    }
+
+    public RuleEngineErrorMsg getError() {
+        return new RuleEngineErrorMsg(md.inMsg.getPayload().getMsgType(), error);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java
new file mode 100644
index 0000000..652cd24
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/ChainProcessingMetaData.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+
+import akka.actor.ActorRef;
+
+/**
+ * Immutable part of chain processing data;
+ *
+ * @author ashvayka
+ */
+public final class ChainProcessingMetaData {
+
+    final RuleActorChain chain;
+    final ToDeviceActorMsg inMsg;
+    final ActorRef originator;
+    final DeviceAttributes deviceAttributes;
+
+    public ChainProcessingMetaData(RuleActorChain chain, ToDeviceActorMsg inMsg, DeviceAttributes deviceAttributes, ActorRef originator) {
+        super();
+        this.chain = chain;
+        this.inMsg = inMsg;
+        this.originator = originator;
+        this.deviceAttributes = deviceAttributes;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java
new file mode 100644
index 0000000..d7c6ce4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/ComplexRuleActorChain.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+public class ComplexRuleActorChain implements RuleActorChain {
+
+    private final RuleActorChain systemChain;
+    private final RuleActorChain tenantChain;
+
+    public ComplexRuleActorChain(RuleActorChain systemChain, RuleActorChain tenantChain) {
+        super();
+        this.systemChain = systemChain;
+        this.tenantChain = tenantChain;
+    }
+
+    @Override
+    public int size() {
+        return systemChain.size() + tenantChain.size();
+    }
+
+    @Override
+    public RuleActorMetaData getRuleActorMd(int index) {
+        if (index < systemChain.size()) {
+            return systemChain.getRuleActorMd(index);
+        } else {
+            return tenantChain.getRuleActorMd(index - systemChain.size());
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java
new file mode 100644
index 0000000..edbd35f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/CompoundRuleActorChain.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+public class CompoundRuleActorChain {
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java
new file mode 100644
index 0000000..f16a444
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActor.java
@@ -0,0 +1,90 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ComponentActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+
+public class RuleActor extends ComponentActor<RuleId, RuleActorMessageProcessor> {
+
+    private RuleActor(ActorSystemContext systemContext, TenantId tenantId, RuleId ruleId) {
+        super(systemContext, tenantId, ruleId);
+        setProcessor(new RuleActorMessageProcessor(tenantId, ruleId, systemContext, logger));
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        logger.debug("[{}] Received message: {}", id, msg);
+        if (msg instanceof RuleProcessingMsg) {
+            try {
+                processor.onRuleProcessingMsg(context(), (RuleProcessingMsg) msg);
+                increaseMessagesProcessedCount();
+            } catch (Exception e) {
+                logAndPersist("onDeviceMsg", e);
+            }
+        } else if (msg instanceof PluginToRuleMsg<?>) {
+            try {
+                processor.onPluginMsg(context(), (PluginToRuleMsg<?>) msg);
+            } catch (Exception e) {
+                logAndPersist("onPluginMsg", e);
+            }
+        } else if (msg instanceof ComponentLifecycleMsg) {
+            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            onClusterEventMsg((ClusterEventMsg) msg);
+        } else if (msg instanceof RuleToPluginTimeoutMsg) {
+            try {
+                processor.onTimeoutMsg(context(), (RuleToPluginTimeoutMsg) msg);
+            } catch (Exception e) {
+                logAndPersist("onTimeoutMsg", e);
+            }
+        } else if (msg instanceof StatsPersistTick) {
+            onStatsPersistTick(id);
+        } else {
+            logger.debug("[{}][{}] Unknown msg type.", tenantId, id, msg.getClass().getName());
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<RuleActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+        private final RuleId ruleId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId, RuleId ruleId) {
+            super(context);
+            this.tenantId = tenantId;
+            this.ruleId = ruleId;
+        }
+
+        @Override
+        public RuleActor create() throws Exception {
+            return new RuleActor(context, tenantId, ruleId);
+        }
+    }
+
+    @Override
+    protected long getErrorPersistFrequency() {
+        return systemContext.getRuleErrorPersistFrequency();
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java
new file mode 100644
index 0000000..ee73d81
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorChain.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+public interface RuleActorChain {
+
+    int size();
+
+    RuleActorMetaData getRuleActorMd(int index);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
new file mode 100644
index 0000000..82011c0
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMessageProcessor.java
@@ -0,0 +1,339 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import java.util.*;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.plugin.RuleToPluginMsgWrapper;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.BasicRequest;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.core.RuleEngineError;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.ex.ProcessingTimeoutException;
+import org.thingsboard.server.extensions.api.rules.*;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+
+class RuleActorMessageProcessor extends ComponentMsgProcessor<RuleId> {
+
+    private final RuleProcessingContext ruleCtx;
+    private final Map<UUID, RuleProcessingMsg> pendingMsgMap;
+
+    private RuleMetaData ruleMd;
+    private ComponentLifecycleState state;
+    private List<RuleFilter> filters;
+    private RuleProcessor processor;
+    private PluginAction action;
+
+    private TenantId pluginTenantId;
+    private PluginId pluginId;
+
+    protected RuleActorMessageProcessor(TenantId tenantId, RuleId ruleId, ActorSystemContext systemContext, LoggingAdapter logger) {
+        super(systemContext, logger, tenantId, ruleId);
+        this.pendingMsgMap = new HashMap<>();
+        this.ruleCtx = new RuleProcessingContext(systemContext, ruleId);
+    }
+
+    @Override
+    public void start() throws Exception {
+        logger.info("[{}][{}] Starting rule actor.", entityId, tenantId);
+        ruleMd = systemContext.getRuleService().findRuleById(entityId);
+        if (ruleMd == null) {
+            throw new RuleInitializationException("Rule not found!");
+        }
+        state = ruleMd.getState();
+        if (state == ComponentLifecycleState.ACTIVE) {
+            logger.info("[{}] Rule is active. Going to initialize rule components.", entityId);
+            initComponent();
+        } else {
+            logger.info("[{}] Rule is suspended. Skipping rule components initialization.", entityId);
+        }
+
+        logger.info("[{}][{}] Started rule actor.", entityId, tenantId);
+    }
+
+    @Override
+    public void stop() throws Exception {
+        onStop();
+    }
+
+
+    private void initComponent() throws RuleException {
+        try {
+            if (!ruleMd.getFilters().isArray()) {
+                throw new RuntimeException("Filters are not array!");
+            }
+            fetchPluginInfo();
+            initFilters();
+            initProcessor();
+            initAction();
+        } catch (RuntimeException e) {
+            throw new RuleInitializationException("Unknown runtime exception!", e);
+        } catch (InstantiationException e) {
+            throw new RuleInitializationException("No default constructor for rule implementation!", e);
+        } catch (IllegalAccessException e) {
+            throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e);
+        } catch (ClassNotFoundException e) {
+            throw new RuleInitializationException("Rule Class not found!", e);
+        } catch (Exception e) {
+            throw new RuleException(e.getMessage(), e);
+        }
+    }
+
+    private void initAction() throws Exception {
+        JsonNode actionMd = ruleMd.getAction();
+        action = initComponent(actionMd);
+    }
+
+    private void initProcessor() throws Exception {
+        if (ruleMd.getProcessor() != null && !ruleMd.getProcessor().isNull()) {
+            processor = initComponent(ruleMd.getProcessor());
+        }
+    }
+
+    private void initFilters() throws Exception {
+        filters = new ArrayList<>(ruleMd.getFilters().size());
+        for (int i = 0; i < ruleMd.getFilters().size(); i++) {
+            filters.add(initComponent(ruleMd.getFilters().get(i)));
+        }
+    }
+
+    private void fetchPluginInfo() {
+        PluginMetaData pluginMd = systemContext.getPluginService().findPluginByApiToken(ruleMd.getPluginToken());
+        pluginTenantId = pluginMd.getTenantId();
+        pluginId = pluginMd.getId();
+    }
+
+    protected void onRuleProcessingMsg(ActorContext context, RuleProcessingMsg msg) throws RuleException {
+        if (state != ComponentLifecycleState.ACTIVE) {
+            pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_ACTIVE_RULES);
+            return;
+        }
+        ChainProcessingContext chainCtx = msg.getCtx();
+        ToDeviceActorMsg inMsg = chainCtx.getInMsg();
+
+        ruleCtx.update(inMsg, chainCtx.getAttributes());
+
+        logger.debug("[{}] Going to filter in msg: {}", entityId, inMsg);
+        for (RuleFilter filter : filters) {
+            if (!filter.filter(ruleCtx, inMsg)) {
+                logger.debug("[{}] In msg is NOT valid for processing by current rule: {}", entityId, inMsg);
+                pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_FILTERS_MATCHED);
+                return;
+            }
+        }
+        RuleProcessingMetaData inMsgMd;
+        if (processor != null) {
+            logger.debug("[{}] Going to process in msg: {}", entityId, inMsg);
+            inMsgMd = processor.process(ruleCtx, inMsg);
+        } else {
+            inMsgMd = new RuleProcessingMetaData();
+        }
+        logger.debug("[{}] Going to convert in msg: {}", entityId, inMsg);
+        Optional<RuleToPluginMsg<?>> ruleToPluginMsgOptional = action.convert(ruleCtx, inMsg, inMsgMd);
+        if (ruleToPluginMsgOptional.isPresent()) {
+            RuleToPluginMsg<?> ruleToPluginMsg = ruleToPluginMsgOptional.get();
+            logger.debug("[{}] Device msg is converter to: {}", entityId, ruleToPluginMsg);
+            context.parent().tell(new RuleToPluginMsgWrapper(pluginTenantId, pluginId, tenantId, entityId, ruleToPluginMsg), context.self());
+            if (action.isOneWayAction()) {
+                pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_TWO_WAY_ACTIONS);
+            } else {
+                pendingMsgMap.put(ruleToPluginMsg.getUid(), msg);
+                scheduleMsgWithDelay(context, new RuleToPluginTimeoutMsg(ruleToPluginMsg.getUid()), systemContext.getPluginProcessingTimeout());
+            }
+        } else {
+            logger.debug("[{}] Nothing to send to plugin: {}", entityId, pluginId);
+            pushToNextRule(context, msg.getCtx(), RuleEngineError.NO_REQUEST_FROM_ACTIONS);
+            return;
+        }
+    }
+
+    public void onPluginMsg(ActorContext context, PluginToRuleMsg<?> msg) {
+        RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getUid());
+        if (pendingMsg != null) {
+            ChainProcessingContext ctx = pendingMsg.getCtx();
+            Optional<ToDeviceMsg> ruleResponseOptional = action.convert(msg);
+            if (ruleResponseOptional.isPresent()) {
+                ctx.mergeResponse(ruleResponseOptional.get());
+                pushToNextRule(context, ctx, null);
+            } else {
+                pushToNextRule(context, ctx, RuleEngineError.NO_RESPONSE_FROM_ACTIONS);
+            }
+        } else {
+            logger.warning("[{}] Processing timeout detected: [{}]", entityId, msg.getUid());
+        }
+    }
+
+    public void onTimeoutMsg(ActorContext context, RuleToPluginTimeoutMsg msg) {
+        RuleProcessingMsg pendingMsg = pendingMsgMap.remove(msg.getMsgId());
+        if (pendingMsg != null) {
+            logger.debug("[{}] Processing timeout detected [{}]: {}", entityId, msg.getMsgId(), pendingMsg);
+            ChainProcessingContext ctx = pendingMsg.getCtx();
+            pushToNextRule(context, ctx, RuleEngineError.PLUGIN_TIMEOUT);
+        }
+    }
+
+    private void pushToNextRule(ActorContext context, ChainProcessingContext ctx, RuleEngineError error) {
+        if (error != null) {
+            ctx = ctx.withError(error);
+        }
+        if (ctx.isFailure()) {
+            logger.debug("[{}] Forwarding processing chain to device actor due to failure.", ctx.getInMsg().getDeviceId());
+            ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender());
+        } else if (!ctx.hasNext()) {
+            logger.debug("[{}] Forwarding processing chain to device actor due to end of chain.", ctx.getInMsg().getDeviceId());
+            ctx.getDeviceActor().tell(new RulesProcessedMsg(ctx), ActorRef.noSender());
+        } else {
+            logger.debug("[{}] Forwarding processing chain to next rule actor.", ctx.getInMsg().getDeviceId());
+            ChainProcessingContext nextTask = ctx.getNext();
+            nextTask.getCurrentActor().tell(new RuleProcessingMsg(nextTask), context.self());
+        }
+    }
+
+    @Override
+    public void onCreated(ActorContext context) {
+        logger.info("[{}] Going to process onCreated rule.", entityId);
+    }
+
+    @Override
+    public void onUpdate(ActorContext context) throws RuleException {
+        RuleMetaData oldRuleMd = ruleMd;
+        ruleMd = systemContext.getRuleService().findRuleById(entityId);
+        logger.info("[{}] Rule configuration was updated from {} to {}.", entityId, oldRuleMd, ruleMd);
+        try {
+            fetchPluginInfo();
+            if (!Objects.equals(oldRuleMd.getFilters(), ruleMd.getFilters())) {
+                logger.info("[{}] Rule filters require restart due to json change from {} to {}.",
+                        entityId, mapper.writeValueAsString(oldRuleMd.getFilters()), mapper.writeValueAsString(ruleMd.getFilters()));
+                stopFilters();
+                initFilters();
+            }
+            if (!Objects.equals(oldRuleMd.getProcessor(), ruleMd.getProcessor())) {
+                logger.info("[{}] Rule processor require restart due to configuration change.", entityId);
+                stopProcessor();
+                initProcessor();
+            }
+            if (!Objects.equals(oldRuleMd.getAction(), ruleMd.getAction())) {
+                logger.info("[{}] Rule action require restart due to configuration change.", entityId);
+                stopAction();
+                initAction();
+            }
+        } catch (RuntimeException e) {
+            throw new RuleInitializationException("Unknown runtime exception!", e);
+        } catch (InstantiationException e) {
+            throw new RuleInitializationException("No default constructor for rule implementation!", e);
+        } catch (IllegalAccessException e) {
+            throw new RuleInitializationException("Illegal Access Exception during rule initialization!", e);
+        } catch (ClassNotFoundException e) {
+            throw new RuleInitializationException("Rule Class not found!", e);
+        } catch (JsonProcessingException e) {
+            throw new RuleInitializationException("Rule configuration is invalid!", e);
+        } catch (Exception e) {
+            throw new RuleInitializationException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public void onActivate(ActorContext context) throws Exception {
+        logger.info("[{}] Going to process onActivate rule.", entityId);
+        this.state = ComponentLifecycleState.ACTIVE;
+        if (action != null) {
+            if (filters != null) {
+                filters.forEach(f -> f.resume());
+            }
+            if (processor != null) {
+                processor.resume();
+            }
+            if (action != null) {
+                action.resume();
+            }
+            logger.info("[{}] Rule resumed.", entityId);
+        } else {
+            start();
+        }
+    }
+
+    @Override
+    public void onSuspend(ActorContext context) {
+        logger.info("[{}] Going to process onSuspend rule.", entityId);
+        this.state = ComponentLifecycleState.SUSPENDED;
+        if (filters != null) {
+            filters.forEach(f -> f.suspend());
+        }
+        if (processor != null) {
+            processor.suspend();
+        }
+        if (action != null) {
+            action.suspend();
+        }
+    }
+
+    @Override
+    public void onStop(ActorContext context) {
+        logger.info("[{}] Going to process onStop rule.", entityId);
+        onStop();
+        scheduleMsgWithDelay(context, new RuleTerminationMsg(entityId), systemContext.getRuleActorTerminationDelay());
+    }
+
+    private void onStop() {
+        this.state = ComponentLifecycleState.SUSPENDED;
+        stopFilters();
+        stopProcessor();
+        stopAction();
+    }
+
+    @Override
+    public void onClusterEventMsg(ClusterEventMsg msg) throws Exception {
+
+    }
+
+    private void stopAction() {
+        if (action != null) {
+            action.stop();
+        }
+    }
+
+    private void stopProcessor() {
+        if (processor != null) {
+            processor.stop();
+        }
+    }
+
+    private void stopFilters() {
+        if (filters != null) {
+            filters.forEach(f -> f.stop());
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java
new file mode 100644
index 0000000..e25c8aa
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleActorMetaData.java
@@ -0,0 +1,107 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import java.util.Comparator;
+
+import org.thingsboard.server.common.data.id.RuleId;
+
+import akka.actor.ActorRef;
+
+public class RuleActorMetaData {
+
+    private final RuleId ruleId;
+    private final boolean systemRule;
+    private final int weight;
+    private final ActorRef actorRef;
+
+    public static final Comparator<RuleActorMetaData> RULE_ACTOR_MD_COMPARATOR = new Comparator<RuleActorMetaData>() {
+
+        @Override
+        public int compare(RuleActorMetaData r1, RuleActorMetaData r2) {
+            if (r1.isSystemRule() && !r2.isSystemRule()) {
+                return 1;
+            } else if (!r1.isSystemRule() && r2.isSystemRule()) {
+                return -1;
+            } else {
+                return Integer.compare(r2.getWeight(), r1.getWeight());
+            }
+        }
+    };
+
+    public static RuleActorMetaData systemRule(RuleId ruleId, int weight, ActorRef actorRef) {
+        return new RuleActorMetaData(ruleId, true, weight, actorRef);
+    }
+
+    public static RuleActorMetaData tenantRule(RuleId ruleId, int weight, ActorRef actorRef) {
+        return new RuleActorMetaData(ruleId, false, weight, actorRef);
+    }
+
+    private RuleActorMetaData(RuleId ruleId, boolean systemRule, int weight, ActorRef actorRef) {
+        super();
+        this.ruleId = ruleId;
+        this.systemRule = systemRule;
+        this.weight = weight;
+        this.actorRef = actorRef;
+    }
+
+    public RuleId getRuleId() {
+        return ruleId;
+    }
+
+    public boolean isSystemRule() {
+        return systemRule;
+    }
+
+    public int getWeight() {
+        return weight;
+    }
+
+    public ActorRef getActorRef() {
+        return actorRef;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((ruleId == null) ? 0 : ruleId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        RuleActorMetaData other = (RuleActorMetaData) obj;
+        if (ruleId == null) {
+            if (other.ruleId != null)
+                return false;
+        } else if (!ruleId.equals(other.ruleId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "RuleActorMetaData [ruleId=" + ruleId + ", systemRule=" + systemRule + ", weight=" + weight + ", actorRef=" + actorRef + "]";
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java
new file mode 100644
index 0000000..507b955
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleContextAwareMsgProcessor.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
+import org.thingsboard.server.common.data.id.RuleId;
+
+import akka.event.LoggingAdapter;
+
+public class RuleContextAwareMsgProcessor extends AbstractContextAwareMsgProcessor {
+
+    private final RuleId ruleId;
+    
+    protected RuleContextAwareMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, RuleId ruleId) {
+        super(systemContext, logger);
+        this.ruleId = ruleId;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java
new file mode 100644
index 0000000..bd07285
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingContext.java
@@ -0,0 +1,89 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+
+import java.util.Optional;
+
+public class RuleProcessingContext implements RuleContext {
+
+    private final TimeseriesService tsService;
+    private final EventService eventService;
+    private final RuleId ruleId;
+    private TenantId tenantId;
+    private CustomerId customerId;
+    private DeviceId deviceId;
+    private DeviceAttributes deviceAttributes;
+
+    RuleProcessingContext(ActorSystemContext systemContext, RuleId ruleId) {
+        this.tsService = systemContext.getTsService();
+        this.eventService = systemContext.getEventService();
+        this.ruleId = ruleId;
+    }
+
+    void update(ToDeviceActorMsg toDeviceActorMsg, DeviceAttributes attributes) {
+        this.tenantId = toDeviceActorMsg.getTenantId();
+        this.customerId = toDeviceActorMsg.getCustomerId();
+        this.deviceId = toDeviceActorMsg.getDeviceId();
+        this.deviceAttributes = attributes;
+    }
+
+    @Override
+    public RuleId getRuleId() {
+        return ruleId;
+    }
+
+    @Override
+    public DeviceAttributes getDeviceAttributes() {
+        return deviceAttributes;
+    }
+
+    @Override
+    public Event save(Event event) {
+        checkEvent(event);
+        return eventService.save(event);
+    }
+
+    @Override
+    public Optional<Event> saveIfNotExists(Event event) {
+        checkEvent(event);
+        return eventService.saveIfNotExists(event);
+    }
+
+    @Override
+    public Optional<Event> findEvent(String eventType, String eventUid) {
+        return eventService.findEvent(tenantId, deviceId, eventType, eventUid);
+    }
+
+    private void checkEvent(Event event) {
+        if (event.getTenantId() == null) {
+            event.setTenantId(tenantId);
+        } else if (!tenantId.equals(event.getTenantId())) {
+            throw new IllegalArgumentException("Invalid Tenant id!");
+        }
+        if (event.getEntityId() == null) {
+            event.setEntityId(deviceId);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java
new file mode 100644
index 0000000..548c180
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleProcessingMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+public class RuleProcessingMsg {
+
+    private final ChainProcessingContext ctx;
+
+    public RuleProcessingMsg(ChainProcessingContext ctx) {
+        super();
+        this.ctx = ctx;
+    }
+
+    public ChainProcessingContext getCtx() {
+        return ctx;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RulesProcessedMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RulesProcessedMsg.java
new file mode 100644
index 0000000..dfebeac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RulesProcessedMsg.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+public class RulesProcessedMsg {
+    private final ChainProcessingContext ctx;
+
+    public RulesProcessedMsg(ChainProcessingContext ctx) {
+        super();
+        this.ctx = ctx;
+    }
+
+    public ChainProcessingContext getCtx() {
+        return ctx;
+    }
+
+    @Override
+    public String toString() {
+        return "RulesProcessedMsg [ctx=" + ctx + "]";
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java
new file mode 100644
index 0000000..2e92005
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleTerminationMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import org.thingsboard.server.actors.shared.ActorTerminationMsg;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RuleTerminationMsg extends ActorTerminationMsg<RuleId> {
+
+    public RuleTerminationMsg(RuleId id) {
+        super(id);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java b/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java
new file mode 100644
index 0000000..38595d6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/RuleToPluginTimeoutMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public class RuleToPluginTimeoutMsg implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final UUID msgId;
+
+    public RuleToPluginTimeoutMsg(UUID msgId) {
+        super();
+        this.msgId = msgId;
+    }
+
+    public UUID getMsgId() {
+        return msgId;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
new file mode 100644
index 0000000..8112ac4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/rule/SimpleRuleActorChain.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.rule;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class SimpleRuleActorChain implements RuleActorChain {
+
+    private final List<RuleActorMetaData> rules;
+
+    public SimpleRuleActorChain(Set<RuleActorMetaData> ruleSet) {
+        rules = new ArrayList<>(ruleSet);
+        Collections.sort(rules, RuleActorMetaData.RULE_ACTOR_MD_COMPARATOR);
+    }
+
+    public int size() {
+        return rules.size();
+    }
+
+    public RuleActorMetaData getRuleActorMd(int index) {
+        return rules.get(index);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
new file mode 100644
index 0000000..1c64f54
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ActorService.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
+import org.thingsboard.server.service.cluster.rpc.RpcMsgListener;
+
+public interface ActorService extends SessionMsgProcessor, WebSocketMsgProcessor, RestMsgProcessor, RpcMsgListener, DiscoveryServiceListener {
+
+    void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state);
+
+    void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state);
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
new file mode 100644
index 0000000..f991552
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ComponentActor.java
@@ -0,0 +1,169 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import akka.actor.ActorRef;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.ComponentMsgProcessor;
+import org.thingsboard.server.actors.stats.StatsPersistMsg;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public abstract class ComponentActor<T extends EntityId, P extends ComponentMsgProcessor<T>> extends ContextAwareActor {
+
+    protected final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+    private long lastPersistedErrorTs = 0L;
+    protected final TenantId tenantId;
+    protected final T id;
+    protected P processor;
+    private long messagesProcessed;
+    private long errorsOccurred;
+
+    public ComponentActor(ActorSystemContext systemContext, TenantId tenantId, T id) {
+        super(systemContext);
+        this.tenantId = tenantId;
+        this.id = id;
+    }
+
+    protected void setProcessor(P processor) {
+        this.processor = processor;
+    }
+
+    @Override
+    public void preStart() {
+        try {
+            processor.start();
+            logLifecycleEvent(ComponentLifecycleEvent.STARTED);
+            if (systemContext.isStatisticsEnabled()) {
+                scheduleStatsPersistTick();
+            }
+        } catch (Exception e) {
+            logger.warning("[{}][{}] Failed to start {} processor: {}", tenantId, id, id.getEntityType(), e);
+            logAndPersist("OnStart", e, true);
+            logLifecycleEvent(ComponentLifecycleEvent.STARTED, e);
+        }
+    }
+
+    private void scheduleStatsPersistTick() {
+        try {
+            processor.scheduleStatsPersistTick(context(), systemContext.getStatisticsPersistFrequency());
+        } catch (Exception e) {
+            logger.error("[{}][{}] Failed to schedule statistics store message. No statistics is going to be stored: {}", tenantId, id, e.getMessage());
+            logAndPersist("onScheduleStatsPersistMsg", e);
+        }
+    }
+
+    @Override
+    public void postStop() {
+        try {
+            processor.stop();
+            logLifecycleEvent(ComponentLifecycleEvent.STOPPED);
+        } catch (Exception e) {
+            logger.warning("[{}][{}] Failed to stop {} processor: {}", tenantId, id, id.getEntityType(), e.getMessage());
+            logAndPersist("OnStop", e, true);
+            logLifecycleEvent(ComponentLifecycleEvent.STOPPED, e);
+        }
+    }
+
+    protected void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
+        try {
+            switch (msg.getEvent()) {
+                case CREATED:
+                    processor.onCreated(context());
+                    break;
+                case UPDATED:
+                    processor.onUpdate(context());
+                    break;
+                case ACTIVATED:
+                    processor.onActivate(context());
+                    break;
+                case SUSPENDED:
+                    processor.onSuspend(context());
+                    break;
+                case DELETED:
+                    processor.onStop(context());
+            }
+            logLifecycleEvent(msg.getEvent());
+        } catch (Exception e) {
+            logAndPersist("onLifecycleMsg", e, true);
+            logLifecycleEvent(msg.getEvent(), e);
+        }
+    }
+
+    protected void onClusterEventMsg(ClusterEventMsg msg) {
+        try {
+            processor.onClusterEventMsg(msg);
+        } catch (Exception e) {
+            logAndPersist("onClusterEventMsg", e);
+        }
+    }
+
+    protected void onStatsPersistTick(EntityId entityId) {
+        try {
+            systemContext.getStatsActor().tell(new StatsPersistMsg(messagesProcessed, errorsOccurred, tenantId, entityId), ActorRef.noSender());
+            resetStatsCounters();
+        } catch (Exception e) {
+            logAndPersist("onStatsPersistTick", e);
+        }
+    }
+
+    private void resetStatsCounters() {
+        messagesProcessed = 0;
+        errorsOccurred = 0;
+    }
+
+    protected void increaseMessagesProcessedCount() {
+        messagesProcessed++;
+    }
+
+
+    protected void logAndPersist(String method, Exception e) {
+        logAndPersist(method, e, false);
+    }
+
+    private void logAndPersist(String method, Exception e, boolean critical) {
+        errorsOccurred++;
+        if (critical) {
+            logger.warning("[{}][{}] Failed to process {} msg: {}", id, tenantId, method, e);
+        } else {
+            logger.debug("[{}][{}] Failed to process {} msg: {}", id, tenantId, method, e);
+        }
+        long ts = System.currentTimeMillis();
+        if (ts - lastPersistedErrorTs > getErrorPersistFrequency()) {
+            systemContext.persistError(tenantId, id, method, e);
+            lastPersistedErrorTs = ts;
+        }
+    }
+
+    protected void logLifecycleEvent(ComponentLifecycleEvent event) {
+        logLifecycleEvent(event, null);
+    }
+
+    protected void logLifecycleEvent(ComponentLifecycleEvent event, Exception e) {
+        systemContext.persistLifecycleEvent(tenantId, id, event, e);
+    }
+
+    protected abstract long getErrorPersistFrequency();
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
new file mode 100644
index 0000000..b2a0de7
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextAwareActor.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import akka.actor.UntypedActor;
+import org.thingsboard.server.actors.ActorSystemContext;
+
+public abstract class ContextAwareActor extends UntypedActor {
+
+    public static final int ENTITY_PACK_LIMIT = 1024;
+
+    protected final ActorSystemContext systemContext;
+
+    public ContextAwareActor(ActorSystemContext systemContext) {
+        super();
+        this.systemContext = systemContext;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
new file mode 100644
index 0000000..7222110
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/ContextBasedCreator.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+
+import akka.japi.Creator;
+
+public abstract class ContextBasedCreator<T> implements Creator<T> {
+
+    private static final long serialVersionUID = 1L;
+
+    protected final ActorSystemContext context;
+
+    public ContextBasedCreator(ActorSystemContext context) {
+        super();
+        this.context = context;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
new file mode 100644
index 0000000..db6526d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/DefaultActorService.java
@@ -0,0 +1,234 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.actor.Props;
+import akka.actor.Terminated;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.app.AppActor;
+import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
+import org.thingsboard.server.actors.rpc.RpcManagerActor;
+import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
+import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
+import org.thingsboard.server.actors.session.SessionManagerActor;
+import org.thingsboard.server.actors.stats.StatsActor;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import scala.concurrent.Await;
+import scala.concurrent.Future;
+import scala.concurrent.duration.Duration;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+@Service
+@Slf4j
+public class DefaultActorService implements ActorService {
+
+    private static final String ACTOR_SYSTEM_NAME = "Akka";
+
+    public static final String APP_DISPATCHER_NAME = "app-dispatcher";
+    public static final String CORE_DISPATCHER_NAME = "core-dispatcher";
+    public static final String RULE_DISPATCHER_NAME = "rule-dispatcher";
+    public static final String PLUGIN_DISPATCHER_NAME = "plugin-dispatcher";
+    public static final String SESSION_DISPATCHER_NAME = "session-dispatcher";
+    public static final String RPC_DISPATCHER_NAME = "rpc-dispatcher";
+
+    @Autowired
+    private ActorSystemContext actorContext;
+
+    @Autowired
+    private ClusterRpcService rpcService;
+
+    @Autowired
+    private DiscoveryService discoveryService;
+
+    private ActorSystem system;
+
+    private ActorRef appActor;
+
+    private ActorRef sessionManagerActor;
+
+    private ActorRef rpcManagerActor;
+
+    @PostConstruct
+    public void initActorSystem() {
+        log.info("Initializing Actor system. {}", actorContext.getRuleService());
+        actorContext.setActorService(this);
+        system = ActorSystem.create(ACTOR_SYSTEM_NAME, actorContext.getConfig());
+        actorContext.setActorSystem(system);
+
+        appActor = system.actorOf(Props.create(new AppActor.ActorCreator(actorContext)).withDispatcher(APP_DISPATCHER_NAME), "appActor");
+        actorContext.setAppActor(appActor);
+
+        sessionManagerActor = system.actorOf(Props.create(new SessionManagerActor.ActorCreator(actorContext)).withDispatcher(CORE_DISPATCHER_NAME),
+                "sessionManagerActor");
+        actorContext.setSessionManagerActor(sessionManagerActor);
+
+        rpcManagerActor = system.actorOf(Props.create(new RpcManagerActor.ActorCreator(actorContext)).withDispatcher(CORE_DISPATCHER_NAME),
+                "rpcManagerActor");
+
+        ActorRef statsActor = system.actorOf(Props.create(new StatsActor.ActorCreator(actorContext)).withDispatcher(CORE_DISPATCHER_NAME), "statsActor");
+        actorContext.setStatsActor(statsActor);
+
+        rpcService.init(this);
+
+        discoveryService.addListener(this);
+        log.info("Actor system initialized.");
+    }
+
+    @PreDestroy
+    public void stopActorSystem() {
+        Future<Terminated> status = system.terminate();
+        try {
+            Terminated terminated = Await.result(status, Duration.Inf());
+            log.info("Actor system terminated: {}", terminated);
+        } catch (Exception e) {
+            log.error("Failed to terminate actor system.", e);
+        }
+    }
+
+    @Override
+    public void process(SessionAwareMsg msg) {
+        if (msg instanceof SessionAwareMsg) {
+            log.debug("Processing session aware msg: {}", msg);
+            sessionManagerActor.tell(msg, ActorRef.noSender());
+        }
+    }
+
+    @Override
+    public void process(PluginWebsocketMsg<?> msg) {
+        log.debug("Processing websocket msg: {}", msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void process(PluginRestMsg msg) {
+        log.debug("Processing rest msg: {}", msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(ToPluginActorMsg msg) {
+        log.trace("Processing plugin rpc msg: {}", msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(ToDeviceActorMsg msg) {
+        log.trace("Processing device rpc msg: {}", msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(ToDeviceActorNotificationMsg msg) {
+        log.trace("Processing notification rpc msg: {}", msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(ToDeviceSessionActorMsg msg) {
+        log.trace("Processing session rpc msg: {}", msg);
+        sessionManagerActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(ToAllNodesMsg msg) {
+        log.trace("Processing broadcast rpc msg: {}", msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(RpcSessionCreateRequestMsg msg) {
+        log.trace("Processing session create msg: {}", msg);
+        rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(RpcSessionTellMsg msg) {
+        log.trace("Processing session rpc msg: {}", msg);
+        rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onMsg(RpcBroadcastMsg msg) {
+        log.trace("Processing broadcast rpc msg: {}", msg);
+        rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+
+    @Override
+    public void onServerAdded(ServerInstance server) {
+        log.trace("Processing onServerAdded msg: {}", server);
+        broadcast(new ClusterEventMsg(server.getServerAddress(), true));
+    }
+
+    @Override
+    public void onServerUpdated(ServerInstance server) {
+
+    }
+
+    @Override
+    public void onServerRemoved(ServerInstance server) {
+        log.trace("Processing onServerRemoved msg: {}", server);
+        broadcast(new ClusterEventMsg(server.getServerAddress(), false));
+    }
+
+    @Override
+    public void onPluginStateChange(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent state) {
+        log.trace("[{}] Processing onPluginStateChange event: {}", pluginId, state);
+        broadcast(ComponentLifecycleMsg.forPlugin(tenantId, pluginId, state));
+    }
+
+    @Override
+    public void onRuleStateChange(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent state) {
+        log.trace("[{}] Processing onRuleStateChange event: {}", ruleId, state);
+        broadcast(ComponentLifecycleMsg.forRule(tenantId, ruleId, state));
+    }
+
+    public void broadcast(ToAllNodesMsg msg) {
+        rpcService.broadcast(msg);
+        appActor.tell(msg, ActorRef.noSender());
+    }
+
+    private void broadcast(ClusterEventMsg msg) {
+        this.appActor.tell(msg, ActorRef.noSender());
+        this.sessionManagerActor.tell(msg, ActorRef.noSender());
+        this.rpcManagerActor.tell(msg, ActorRef.noSender());
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/RestMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/service/RestMsgProcessor.java
new file mode 100644
index 0000000..44c60e5
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/RestMsgProcessor.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+
+public interface RestMsgProcessor {
+
+    void process(PluginRestMsg msg);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java
new file mode 100644
index 0000000..f8ebda3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/service/WebSocketMsgProcessor.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.service;
+
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+public interface WebSocketMsgProcessor {
+
+    void process(PluginWebsocketMsg<?> msg);
+    
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
new file mode 100644
index 0000000..57adf8c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/AbstractSessionActorMsgProcessor.java
@@ -0,0 +1,119 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.session;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.AbstractContextAwareMsgProcessor;
+import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.device.BasicToDeviceActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.event.LoggingAdapter;
+
+import java.util.Optional;
+
+abstract class AbstractSessionActorMsgProcessor extends AbstractContextAwareMsgProcessor {
+
+    protected final SessionId sessionId;
+    protected SessionContext sessionCtx;
+    protected ToDeviceActorMsg toDeviceActorMsgPrototype;
+
+    protected AbstractSessionActorMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
+        super(ctx, logger);
+        this.sessionId = sessionId;
+    }
+
+    protected abstract void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg);
+
+    protected abstract void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg);
+
+    protected abstract void processToDeviceMsg(ActorContext context, ToDeviceMsg msg);
+
+    public abstract void processClusterEvent(ActorContext context, ClusterEventMsg msg);
+
+    protected void processSessionCtrlMsg(ActorContext ctx, SessionCtrlMsg msg) {
+        if (msg instanceof SessionCloseMsg) {
+            cleanupSession(ctx);
+            terminateSession(ctx, sessionId);
+        }
+    }
+
+    protected void cleanupSession(ActorContext ctx) {
+    }
+
+    protected void updateSessionCtx(ToDeviceActorSessionMsg msg, SessionType type) {
+        sessionCtx = msg.getSessionMsg().getSessionContext();
+        toDeviceActorMsgPrototype = new BasicToDeviceActorMsg(msg, type);
+    }
+
+    protected ToDeviceActorMsg toDeviceMsg(ToDeviceActorSessionMsg msg) {
+        AdaptorToSessionActorMsg adaptorMsg = msg.getSessionMsg();
+        return new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, adaptorMsg.getMsg());
+    }
+
+    protected Optional<ToDeviceActorMsg> toDeviceMsg(FromDeviceMsg msg) {
+        if (toDeviceActorMsgPrototype != null) {
+            return Optional.of(new BasicToDeviceActorMsg(toDeviceActorMsgPrototype, msg));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    protected Optional<ServerAddress> forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward) {
+        Optional<ServerAddress> address = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+        forwardToAppActor(ctx, toForward, address);
+        return address;
+    }
+
+    protected Optional<ServerAddress> forwardToAppActorIfAdressChanged(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> oldAddress) {
+        Optional<ServerAddress> newAddress = systemContext.getRoutingService().resolve(toForward.getDeviceId());
+        if (!newAddress.equals(oldAddress)) {
+            if (newAddress.isPresent()) {
+                systemContext.getRpcService().tell(newAddress.get(),
+                        toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
+            } else {
+                getAppActor().tell(toForward, ctx.self());
+            }
+        }
+        return newAddress;
+    }
+
+    protected void forwardToAppActor(ActorContext ctx, ToDeviceActorMsg toForward, Optional<ServerAddress> address) {
+        if (address.isPresent()) {
+            systemContext.getRpcService().tell(address.get(),
+                    toForward.toOtherAddress(systemContext.getRoutingService().getCurrentServer()));
+        } else {
+            getAppActor().tell(toForward, ctx.self());
+        }
+    }
+
+    public static void terminateSession(ActorContext ctx, SessionId sessionId) {
+        ctx.parent().tell(new SessionTerminationMsg(sessionId), ActorRef.noSender());
+        ctx.stop(ctx.self());
+    }
+
+    public DeviceId getDeviceId() {
+        return toDeviceActorMsgPrototype.getDeviceId();
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
new file mode 100644
index 0000000..eb812df
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/ASyncMsgProcessor.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.session;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.core.AttributesSubscribeMsg;
+import org.thingsboard.server.common.msg.core.ResponseMsg;
+import org.thingsboard.server.common.msg.core.RpcSubscribeMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.*;
+
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+class ASyncMsgProcessor extends AbstractSessionActorMsgProcessor {
+
+    Map<Integer, ToDeviceActorMsg> pendingMap = new HashMap<>();
+    private Optional<ServerAddress> currentTargetServer;
+    private boolean subscribedToAttributeUpdates;
+    private boolean subscribedToRpcCommands;
+
+    public ASyncMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
+        super(ctx, logger, sessionId);
+    }
+
+    @Override
+    protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+        updateSessionCtx(msg, SessionType.ASYNC);
+        ToDeviceActorMsg pendingMsg = toDeviceMsg(msg);
+        FromDeviceMsg fromDeviceMsg = pendingMsg.getPayload();
+        switch (fromDeviceMsg.getMsgType()) {
+            case POST_TELEMETRY_REQUEST:
+            case POST_ATTRIBUTES_REQUEST:
+                FromDeviceRequestMsg requestMsg = (FromDeviceRequestMsg) fromDeviceMsg;
+                if (requestMsg.getRequestId() >= 0) {
+                    logger.debug("[{}] Pending request {} registered", requestMsg.getRequestId(), requestMsg.getMsgType());
+                    //TODO: handle duplicates.
+                    pendingMap.put(requestMsg.getRequestId(), pendingMsg);
+                }
+                break;
+            case SUBSCRIBE_ATTRIBUTES_REQUEST:
+                subscribedToAttributeUpdates = true;
+                break;
+            case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+                subscribedToAttributeUpdates = false;
+                break;
+            case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+                subscribedToRpcCommands = true;
+                break;
+            case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+                subscribedToRpcCommands = false;
+                break;
+        }
+        currentTargetServer = forwardToAppActor(ctx, pendingMsg);
+    }
+
+    @Override
+    public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
+        try {
+            switch (msg.getMsgType()) {
+                case STATUS_CODE_RESPONSE:
+                case GET_ATTRIBUTES_RESPONSE:
+                    ResponseMsg responseMsg = (ResponseMsg) msg;
+                    if (responseMsg.getRequestId() >= 0) {
+                        logger.debug("[{}] Pending request processed: {}", responseMsg.getRequestId(), responseMsg);
+                        pendingMap.remove(responseMsg.getRequestId());
+                    }
+                    break;
+            }
+            sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+        } catch (SessionException e) {
+            logger.warning("Failed to push session response msg", e);
+        }
+    }
+
+    @Override
+    public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
+        // TODO Auto-generated method stub        
+    }
+
+    protected void cleanupSession(ActorContext ctx) {
+        toDeviceMsg(new SessionCloseMsg()).ifPresent(msg -> forwardToAppActor(ctx, msg));
+    }
+
+    @Override
+    public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
+        if (pendingMap.size() > 0 || subscribedToAttributeUpdates || subscribedToRpcCommands) {
+            Optional<ServerAddress> newTargetServer = systemContext.getRoutingService().resolve(getDeviceId());
+            if (!newTargetServer.equals(currentTargetServer)) {
+                currentTargetServer = newTargetServer;
+                pendingMap.values().stream().forEach(v -> {
+                    forwardToAppActor(context, v, currentTargetServer);
+                    if (currentTargetServer.isPresent()) {
+                        logger.debug("[{}] Forwarded msg to new server: {}", sessionId, currentTargetServer.get());
+                    } else {
+                        logger.debug("[{}] Forwarded msg to local server.", sessionId);
+                    }
+                });
+                if (subscribedToAttributeUpdates) {
+                    toDeviceMsg(new AttributesSubscribeMsg()).ifPresent(m -> forwardToAppActor(context, m, currentTargetServer));
+                    logger.debug("[{}] Forwarded attributes subscription.", sessionId);
+                }
+                if (subscribedToRpcCommands) {
+                    toDeviceMsg(new RpcSubscribeMsg()).ifPresent(m -> forwardToAppActor(context, m, currentTargetServer));
+                    logger.debug("[{}] Forwarded rpc commands subscription.", sessionId);
+                }
+            }
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
new file mode 100644
index 0000000..30f5d99
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionActor.java
@@ -0,0 +1,139 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.session;
+
+import akka.actor.OneForOneStrategy;
+import akka.actor.SupervisorStrategy;
+import akka.japi.Function;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import scala.concurrent.duration.Duration;
+
+public class SessionActor extends ContextAwareActor {
+
+    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+    private final SessionId sessionId;
+    private AbstractSessionActorMsgProcessor processor;
+
+    private SessionActor(ActorSystemContext systemContext, SessionId sessionId) {
+        super(systemContext);
+        this.sessionId = sessionId;
+    }
+
+    @Override
+    public SupervisorStrategy supervisorStrategy() {
+        return new OneForOneStrategy(-1, Duration.Inf(),
+                throwable -> {
+                    logger.error(throwable, "Unknown session error");
+                    if (throwable instanceof Error) {
+                        return OneForOneStrategy.escalate();
+                    } else {
+                        return OneForOneStrategy.resume();
+                    }
+                });
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        logger.debug("[{}] Processing: {}.", sessionId, msg);
+        if (msg instanceof ToDeviceActorSessionMsg) {
+            processDeviceMsg((ToDeviceActorSessionMsg) msg);
+        } else if (msg instanceof ToDeviceSessionActorMsg) {
+            processToDeviceMsg((ToDeviceSessionActorMsg) msg);
+        } else if (msg instanceof SessionTimeoutMsg) {
+            processTimeoutMsg((SessionTimeoutMsg) msg);
+        } else if (msg instanceof SessionCtrlMsg) {
+            processSessionCtrlMsg((SessionCtrlMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            processClusterEvent((ClusterEventMsg) msg);
+        } else {
+            logger.warning("[{}] Unknown msg: {}", sessionId, msg);
+        }
+    }
+
+    private void processClusterEvent(ClusterEventMsg msg) {
+        processor.processClusterEvent(context(), msg);
+    }
+
+    private void processDeviceMsg(ToDeviceActorSessionMsg msg) {
+        initProcessor(msg);
+        processor.processToDeviceActorMsg(context(), msg);
+    }
+
+    private void processToDeviceMsg(ToDeviceSessionActorMsg msg) {
+        processor.processToDeviceMsg(context(), msg.getMsg());
+    }
+
+    private void processTimeoutMsg(SessionTimeoutMsg msg) {
+        if (processor != null) {
+            processor.processTimeoutMsg(context(), msg);
+        } else {
+            logger.warning("[{}] Can't process timeout msg: {} without processor", sessionId, msg);
+        }
+    }
+
+    private void processSessionCtrlMsg(SessionCtrlMsg msg) {
+        if (processor != null) {
+            processor.processSessionCtrlMsg(context(), msg);
+        } else if (msg instanceof SessionCloseMsg) {
+            AbstractSessionActorMsgProcessor.terminateSession(context(), sessionId);
+        } else {
+            logger.warning("[{}] Can't process session ctrl msg: {} without processor", sessionId, msg);
+        }
+    }
+
+    private void initProcessor(ToDeviceActorSessionMsg msg) {
+        if (processor == null) {
+            SessionMsg sessionMsg = (SessionMsg) msg.getSessionMsg();
+            if (sessionMsg.getSessionContext().getSessionType() == SessionType.SYNC) {
+                processor = new SyncMsgProcessor(systemContext, logger, sessionId);
+            } else {
+                processor = new ASyncMsgProcessor(systemContext, logger, sessionId);
+            }
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<SessionActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final SessionId sessionId;
+
+        public ActorCreator(ActorSystemContext context, SessionId sessionId) {
+            super(context);
+            this.sessionId = sessionId;
+        }
+
+        @Override
+        public SessionActor create() throws Exception {
+            return new SessionActor(context, sessionId);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
new file mode 100644
index 0000000..44eff16
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionManagerActor.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.session;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+import akka.actor.*;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.core.SessionCloseMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+
+public class SessionManagerActor extends ContextAwareActor {
+
+    private static final int INITIAL_SESSION_MAP_SIZE = 1024;
+
+    private final LoggingAdapter log = Logging.getLogger(getContext().system(), this);
+
+    private final Map<String, ActorRef> sessionActors;
+
+    public SessionManagerActor(ActorSystemContext systemContext) {
+        super(systemContext);
+        this.sessionActors = new HashMap<>(INITIAL_SESSION_MAP_SIZE);
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        if (msg instanceof SessionAwareMsg) {
+            forwardToSessionActor((SessionAwareMsg) msg);
+        } else if (msg instanceof SessionTerminationMsg) {
+            onSessionTermination((SessionTerminationMsg) msg);
+        } else if (msg instanceof Terminated) {
+            onTermination((Terminated) msg);
+        } else if (msg instanceof SessionTimeoutMsg) {
+            onSessionTimeout((SessionTimeoutMsg) msg);
+        } else if (msg instanceof SessionCtrlMsg) {
+            onSessionCtrlMsg((SessionCtrlMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            broadcast(msg);
+        }
+    }
+
+    private void broadcast(Object msg) {
+        sessionActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+    }
+
+    private void onSessionTimeout(SessionTimeoutMsg msg) {
+        String sessionIdStr = msg.getSessionId().toUidStr();
+        ActorRef sessionActor = sessionActors.get(sessionIdStr);
+        if (sessionActor != null) {
+            sessionActor.tell(msg, ActorRef.noSender());
+        }
+    }
+
+    private void onSessionCtrlMsg(SessionCtrlMsg msg) {
+        String sessionIdStr = msg.getSessionId().toUidStr();
+        ActorRef sessionActor = sessionActors.get(sessionIdStr);
+        if (sessionActor != null) {
+            sessionActor.tell(msg, ActorRef.noSender());
+        }
+    }
+
+    private void onSessionTermination(SessionTerminationMsg msg) {
+        String sessionIdStr = msg.getId().toUidStr();
+        ActorRef sessionActor = sessionActors.remove(sessionIdStr);
+        if (sessionActor != null) {
+            log.debug("[{}] Removed session actor.", sessionIdStr);
+            //TODO: onSubscriptionUpdate device actor about session close;
+        } else {
+            log.debug("[{}] Session actor was already removed.", sessionIdStr);
+        }
+    }
+
+    private void forwardToSessionActor(SessionAwareMsg msg) {
+        if (msg instanceof ToDeviceSessionActorMsg || msg instanceof SessionCloseMsg) {
+            String sessionIdStr = msg.getSessionId().toUidStr();
+            ActorRef sessionActor = sessionActors.get(sessionIdStr);
+            if (sessionActor != null) {
+                sessionActor.tell(msg, ActorRef.noSender());
+            } else {
+                log.debug("[{}] Session actor was already removed.", sessionIdStr);
+            }
+        } else {
+            try {
+                getOrCreateSessionActor(msg.getSessionId()).tell(msg, self());
+            } catch (InvalidActorNameException e) {
+                log.info("Invalid msg : {}", msg);
+            }
+        }
+    }
+
+    private ActorRef getOrCreateSessionActor(SessionId sessionId) {
+        String sessionIdStr = sessionId.toUidStr();
+        ActorRef sessionActor = sessionActors.get(sessionIdStr);
+        if (sessionActor == null) {
+            log.debug("[{}] Creating session actor.", sessionIdStr);
+            sessionActor = context().actorOf(
+                    Props.create(new SessionActor.ActorCreator(systemContext, sessionId)).withDispatcher(DefaultActorService.SESSION_DISPATCHER_NAME),
+                    sessionIdStr);
+            sessionActors.put(sessionIdStr, sessionActor);
+            log.debug("[{}] Created session actor.", sessionIdStr);
+        }
+        return sessionActor;
+    }
+
+    private void onTermination(Terminated message) {
+        ActorRef terminated = message.actor();
+        if (terminated instanceof LocalActorRef) {
+            log.info("Removed actor: {}.", terminated);
+            //TODO: cleanup session actors map
+        } else {
+            throw new IllegalStateException("Remote actors are not supported!");
+        }
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<SessionManagerActor> {
+        private static final long serialVersionUID = 1L;
+
+        public ActorCreator(ActorSystemContext context) {
+            super(context);
+        }
+
+        @Override
+        public SessionManagerActor create() throws Exception {
+            return new SessionManagerActor(context);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java
new file mode 100644
index 0000000..365726b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SessionTerminationMsg.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.session;
+
+import org.thingsboard.server.actors.shared.ActorTerminationMsg;
+import org.thingsboard.server.common.data.id.SessionId;
+
+public class SessionTerminationMsg extends ActorTerminationMsg<SessionId> {
+
+    public SessionTerminationMsg(SessionId id) {
+        super(id);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
new file mode 100644
index 0000000..afb35ac
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/session/SyncMsgProcessor.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.session;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.shared.SessionTimeoutMsg;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
+
+import java.util.Optional;
+
+class SyncMsgProcessor extends AbstractSessionActorMsgProcessor {
+    private ToDeviceActorMsg pendingMsg;
+    private Optional<ServerAddress> currentTargetServer;
+    private boolean pendingResponse;
+
+    public SyncMsgProcessor(ActorSystemContext ctx, LoggingAdapter logger, SessionId sessionId) {
+        super(ctx, logger, sessionId);
+    }
+
+    @Override
+    protected void processToDeviceActorMsg(ActorContext ctx, ToDeviceActorSessionMsg msg) {
+        updateSessionCtx(msg, SessionType.SYNC);
+        pendingMsg = toDeviceMsg(msg);
+        pendingResponse = true;
+        currentTargetServer = forwardToAppActor(ctx, pendingMsg);
+        scheduleMsgWithDelay(ctx, new SessionTimeoutMsg(sessionId), getTimeout(systemContext, msg.getSessionMsg().getSessionContext()), ctx.parent());
+    }
+
+    public void processTimeoutMsg(ActorContext context, SessionTimeoutMsg msg) {
+        if (pendingResponse) {
+            try {
+                sessionCtx.onMsg(new SessionCloseMsg(sessionId, true));
+            } catch (SessionException e) {
+                logger.warning("Failed to push session close msg", e);
+            }
+            terminateSession(context, this.sessionId);
+        }
+    }
+
+    public void processToDeviceMsg(ActorContext context, ToDeviceMsg msg) {
+        try {
+            sessionCtx.onMsg(new BasicSessionActorToAdaptorMsg(this.sessionCtx, msg));
+            pendingResponse = false;
+        } catch (SessionException e) {
+            logger.warning("Failed to push session response msg", e);
+        }
+        terminateSession(context, this.sessionId);
+    }
+
+    @Override
+    public void processClusterEvent(ActorContext context, ClusterEventMsg msg) {
+        if (pendingResponse) {
+            Optional<ServerAddress> newTargetServer = forwardToAppActorIfAdressChanged(context, pendingMsg, currentTargetServer);
+            if (logger.isDebugEnabled()) {
+                if (!newTargetServer.equals(currentTargetServer)) {
+                    if (newTargetServer.isPresent()) {
+                        logger.debug("[{}] Forwarded msg to new server: {}", sessionId, newTargetServer.get());
+                    } else {
+                        logger.debug("[{}] Forwarded msg to local server.", sessionId);
+                    }
+                }
+            }
+            currentTargetServer = newTargetServer;
+        }
+    }
+
+    private long getTimeout(ActorSystemContext ctx, SessionContext sessionCtx) {
+        return sessionCtx.getTimeout() > 0 ? sessionCtx.getTimeout() : ctx.getSyncSessionTimeout();
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
new file mode 100644
index 0000000..1c7f687
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/AbstractContextAwareMsgProcessor.java
@@ -0,0 +1,135 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Scheduler;
+import akka.event.LoggingAdapter;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.extensions.api.component.*;
+import scala.concurrent.ExecutionContextExecutor;
+import scala.concurrent.duration.Duration;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+public abstract class AbstractContextAwareMsgProcessor {
+
+    protected final ActorSystemContext systemContext;
+    protected final LoggingAdapter logger;
+    protected final ObjectMapper mapper = new ObjectMapper();
+
+    protected AbstractContextAwareMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger) {
+        super();
+        this.systemContext = systemContext;
+        this.logger = logger;
+    }
+
+    protected ActorRef getAppActor() {
+        return systemContext.getAppActor();
+    }
+
+    protected Scheduler getScheduler() {
+        return systemContext.getScheduler();
+    }
+
+    protected ExecutionContextExecutor getSystemDispatcher() {
+        return systemContext.getActorSystem().dispatcher();
+    }
+
+    protected void schedulePeriodicMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, long periodInMs) {
+        schedulePeriodicMsgWithDelay(ctx, msg, delayInMs, periodInMs, ctx.self());
+    }
+
+    protected void schedulePeriodicMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, long periodInMs, ActorRef target) {
+        logger.debug("Scheduling periodic msg {} every {} ms with delay {} ms", msg, periodInMs, delayInMs);
+        getScheduler().schedule(Duration.create(delayInMs, TimeUnit.MILLISECONDS), Duration.create(periodInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
+    }
+
+
+    protected void scheduleMsgWithDelay(ActorContext ctx, Object msg, long delayInMs) {
+        scheduleMsgWithDelay(ctx, msg, delayInMs, ctx.self());
+    }
+
+    protected void scheduleMsgWithDelay(ActorContext ctx, Object msg, long delayInMs, ActorRef target) {
+        logger.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
+        getScheduler().scheduleOnce(Duration.create(delayInMs, TimeUnit.MILLISECONDS), target, msg, getSystemDispatcher(), null);
+    }
+
+    protected <T extends ConfigurableComponent> T initComponent(JsonNode componentNode) throws Exception {
+        ComponentConfiguration configuration = new ComponentConfiguration(
+                componentNode.get("clazz").asText(),
+                componentNode.get("name").asText(),
+                mapper.writeValueAsString(componentNode.get("configuration"))
+        );
+        logger.info("Initializing [{}][{}] component", configuration.getName(), configuration.getClazz());
+        ComponentDescriptor componentDescriptor = systemContext.getComponentService().getComponent(configuration.getClazz())
+                .orElseThrow(() -> new InstantiationException("Component Not found!"));
+        return initComponent(componentDescriptor, configuration);
+    }
+
+    protected <T extends ConfigurableComponent> T initComponent(ComponentDescriptor componentDefinition, ComponentConfiguration configuration)
+            throws Exception {
+        return initComponent(componentDefinition.getClazz(), componentDefinition.getType(), configuration.getConfiguration());
+    }
+
+    protected <T extends ConfigurableComponent> T initComponent(String clazz, ComponentType type, String configuration)
+            throws Exception {
+        Class<?> componentClazz = Class.forName(clazz);
+        T component = (T) (componentClazz.newInstance());
+        Class<?> configurationClazz;
+        switch (type) {
+            case FILTER:
+                configurationClazz = ((Filter) componentClazz.getAnnotation(Filter.class)).configuration();
+                break;
+            case PROCESSOR:
+                configurationClazz = ((Processor) componentClazz.getAnnotation(Processor.class)).configuration();
+                break;
+            case ACTION:
+                configurationClazz = ((Action) componentClazz.getAnnotation(Action.class)).configuration();
+                break;
+            case PLUGIN:
+                configurationClazz = ((Plugin) componentClazz.getAnnotation(Plugin.class)).configuration();
+                break;
+            default:
+                throw new IllegalStateException("Component with type: " + type + " is not supported!");
+        }
+        component.init(decode(configuration, configurationClazz));
+        return component;
+    }
+
+    public <C> C decode(String configuration, Class<C> configurationClazz) throws IOException, RuntimeException {
+        logger.info("Initializing using configuration: {}", configuration);
+        return mapper.readValue(configuration, configurationClazz);
+    }
+
+    @Data
+    @AllArgsConstructor
+    private static class ComponentConfiguration {
+        private final String clazz;
+        private final String name;
+        private final String configuration;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ActorTerminationMsg.java b/application/src/main/java/org/thingsboard/server/actors/shared/ActorTerminationMsg.java
new file mode 100644
index 0000000..ed94e2c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ActorTerminationMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared;
+
+public abstract class ActorTerminationMsg<T> {
+
+    private final T id;
+
+    public ActorTerminationMsg(T id) {
+        super();
+        this.id = id;
+    }
+
+    public T getId() {
+        return id;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
new file mode 100644
index 0000000..2afd619
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/ComponentMsgProcessor.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared;
+
+import akka.actor.ActorContext;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.stats.StatsPersistTick;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+
+public abstract class ComponentMsgProcessor<T> extends AbstractContextAwareMsgProcessor {
+
+    protected final TenantId tenantId;
+    protected final T entityId;
+
+    protected ComponentMsgProcessor(ActorSystemContext systemContext, LoggingAdapter logger, TenantId tenantId, T id) {
+        super(systemContext, logger);
+        this.tenantId = tenantId;
+        this.entityId = id;
+    }
+
+    public abstract void start() throws Exception;
+
+    public abstract void stop() throws Exception;
+
+    public abstract void onCreated(ActorContext context) throws Exception;
+
+    public abstract void onUpdate(ActorContext context) throws Exception;
+
+    public abstract void onActivate(ActorContext context) throws Exception;
+
+    public abstract void onSuspend(ActorContext context) throws Exception;
+
+    public abstract void onStop(ActorContext context) throws Exception;
+
+    public abstract void onClusterEventMsg(ClusterEventMsg msg) throws Exception;
+
+    public void scheduleStatsPersistTick(ActorContext context, long statsPersistFrequency) {
+        schedulePeriodicMsgWithDelay(context, new StatsPersistTick(), statsPersistFrequency, statsPersistFrequency);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
new file mode 100644
index 0000000..c581c41
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/PluginManager.java
@@ -0,0 +1,83 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.plugin;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.plugin.PluginActor;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+
+@Slf4j
+public abstract class PluginManager {
+
+    protected final ActorSystemContext systemContext;
+    protected final PluginService pluginService;
+    protected final Map<PluginId, ActorRef> pluginActors;
+
+    public PluginManager(ActorSystemContext systemContext) {
+        this.systemContext = systemContext;
+        this.pluginService = systemContext.getPluginService();
+        this.pluginActors = new HashMap<>();
+    }
+
+    public void init(ActorContext context) {
+        PageDataIterable<PluginMetaData> pluginIterator = new PageDataIterable<>(getFetchPluginsFunction(),
+                ContextAwareActor.ENTITY_PACK_LIMIT);
+        for (PluginMetaData plugin : pluginIterator) {
+            log.debug("[{}] Creating plugin actor", plugin.getId());
+            getOrCreatePluginActor(context, plugin.getId());
+            log.debug("Plugin actor created.");
+        }
+    }
+
+    abstract FetchFunction<PluginMetaData> getFetchPluginsFunction();
+
+    abstract TenantId getTenantId();
+
+    public ActorRef getOrCreatePluginActor(ActorContext context, PluginId pluginId) {
+        ActorRef pluginActor = pluginActors.get(pluginId);
+        if (pluginActor == null) {
+            pluginActor = context.actorOf(Props.create(new PluginActor.ActorCreator(systemContext, getTenantId(), pluginId))
+                    .withDispatcher(DefaultActorService.PLUGIN_DISPATCHER_NAME), pluginId.toString());
+            pluginActors.put(pluginId, pluginActor);
+        }
+        return pluginActor;
+    }
+
+    public void broadcast(Object msg) {
+        pluginActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+    }
+
+    public void remove(PluginId id) {
+        pluginActors.remove(id);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
new file mode 100644
index 0000000..d8b58a0
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/SystemPluginManager.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.plugin;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.dao.plugin.BasePluginService;
+import org.thingsboard.server.dao.plugin.PluginService;
+
+public class SystemPluginManager extends PluginManager {
+
+    public SystemPluginManager(ActorSystemContext systemContext) {
+        super(systemContext);
+    }
+
+    @Override
+    FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+        return link -> pluginService.findSystemPlugins(link);
+    }
+
+    @Override
+    TenantId getTenantId() {
+        return BasePluginService.SYSTEM_TENANT;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
new file mode 100644
index 0000000..dbff415
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/plugin/TenantPluginManager.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.plugin;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+
+public class TenantPluginManager extends PluginManager {
+
+    private final TenantId tenantId;
+
+    public TenantPluginManager(ActorSystemContext systemContext, TenantId tenantId) {
+        super(systemContext);
+        this.tenantId = tenantId;
+    }
+
+    @Override
+    FetchFunction<PluginMetaData> getFetchPluginsFunction() {
+        return link -> pluginService.findTenantPlugins(tenantId, link);
+    }
+
+    @Override
+    TenantId getTenantId() {
+        return tenantId;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
new file mode 100644
index 0000000..67d44e9
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/RuleManager.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.rule;
+
+import akka.actor.ActorContext;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.rule.RuleActor;
+import org.thingsboard.server.actors.rule.RuleActorChain;
+import org.thingsboard.server.actors.rule.RuleActorMetaData;
+import org.thingsboard.server.actors.rule.SimpleRuleActorChain;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.rule.RuleService;
+
+import java.util.*;
+
+public abstract class RuleManager {
+
+    protected static final Logger logger = LoggerFactory.getLogger(RuleManager.class);
+
+    protected final ActorSystemContext systemContext;
+    protected final RuleService ruleService;
+    protected final Map<RuleId, ActorRef> ruleActors;
+    protected final TenantId tenantId;
+
+    Map<RuleMetaData, RuleActorMetaData> ruleMap = new HashMap<>();
+    private RuleActorChain ruleChain;
+
+    public RuleManager(ActorSystemContext systemContext, TenantId tenantId) {
+        this.systemContext = systemContext;
+        this.ruleService = systemContext.getRuleService();
+        this.ruleActors = new HashMap<>();
+        this.tenantId = tenantId;
+    }
+
+    public void init(ActorContext context) {
+        PageDataIterable<RuleMetaData> ruleIterator = new PageDataIterable<>(getFetchRulesFunction(),
+                ContextAwareActor.ENTITY_PACK_LIMIT);
+        ruleMap = new HashMap<>();
+
+        for (RuleMetaData rule : ruleIterator) {
+            logger.debug("[{}] Creating rule actor {}", rule.getId(), rule);
+            ActorRef ref = getOrCreateRuleActor(context, rule.getId());
+            RuleActorMetaData actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref);
+            ruleMap.put(rule, actorMd);
+            logger.debug("[{}] Rule actor created.", rule.getId());
+        }
+
+        refreshRuleChain();
+    }
+
+    public Optional<ActorRef> update(ActorContext context, RuleId ruleId, ComponentLifecycleEvent event) {
+        RuleMetaData rule = null;
+        if (event != ComponentLifecycleEvent.DELETED) {
+            rule = systemContext.getRuleService().findRuleById(ruleId);
+        }
+        if (rule == null) {
+            rule = ruleMap.keySet().stream().filter(r -> r.getId().equals(ruleId)).findFirst().orElse(null);
+            rule.setState(ComponentLifecycleState.SUSPENDED);
+        }
+        if (rule != null) {
+            RuleActorMetaData actorMd = ruleMap.get(rule);
+            if (actorMd == null) {
+                ActorRef ref = getOrCreateRuleActor(context, rule.getId());
+                actorMd = RuleActorMetaData.systemRule(rule.getId(), rule.getWeight(), ref);
+                ruleMap.put(rule, actorMd);
+            }
+            refreshRuleChain();
+            return Optional.of(actorMd.getActorRef());
+        } else {
+            logger.warn("[{}] Can't process unknown rule!", rule.getId());
+            return Optional.empty();
+        }
+    }
+
+    abstract FetchFunction<RuleMetaData> getFetchRulesFunction();
+
+    public ActorRef getOrCreateRuleActor(ActorContext context, RuleId ruleId) {
+        ActorRef ruleActor = ruleActors.get(ruleId);
+        if (ruleActor == null) {
+            ruleActor = context.actorOf(Props.create(new RuleActor.ActorCreator(systemContext, tenantId, ruleId))
+                    .withDispatcher(DefaultActorService.RULE_DISPATCHER_NAME), ruleId.toString());
+            ruleActors.put(ruleId, ruleActor);
+        }
+        return ruleActor;
+    }
+
+    public RuleActorChain getRuleChain() {
+        return ruleChain;
+    }
+
+
+    private void refreshRuleChain() {
+        Set<RuleActorMetaData> activeRuleSet = new HashSet<>();
+        for (Map.Entry<RuleMetaData, RuleActorMetaData> rule : ruleMap.entrySet()) {
+            if (rule.getKey().getState() == ComponentLifecycleState.ACTIVE) {
+                activeRuleSet.add(rule.getValue());
+            }
+        }
+        ruleChain = new SimpleRuleActorChain(activeRuleSet);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
new file mode 100644
index 0000000..6d56832
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/SystemRuleManager.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.dao.model.ModelConstants;
+
+public class SystemRuleManager extends RuleManager {
+
+    public SystemRuleManager(ActorSystemContext systemContext) {
+        super(systemContext, new TenantId(ModelConstants.NULL_UUID));
+    }
+
+    @Override
+    FetchFunction<RuleMetaData> getFetchRulesFunction() {
+        return link -> ruleService.findSystemRules(link);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java b/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java
new file mode 100644
index 0000000..700614d
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/rule/TenantRuleManager.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared.rule;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.PageDataIterable.FetchFunction;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+
+public class TenantRuleManager extends RuleManager {
+    
+    public TenantRuleManager(ActorSystemContext systemContext, TenantId tenantId) {
+        super(systemContext, tenantId);
+    }
+
+    @Override
+    FetchFunction<RuleMetaData> getFetchRulesFunction() {
+        return link -> ruleService.findTenantRules(tenantId, link);
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
new file mode 100644
index 0000000..2305a35
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/shared/SessionTimeoutMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.shared;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.SessionId;
+
+import java.io.Serializable;
+
+@Data
+public class SessionTimeoutMsg implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final SessionId sessionId;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
new file mode 100644
index 0000000..8b59f70
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsActor.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.stats;
+
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+public class StatsActor extends ContextAwareActor {
+
+    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    public StatsActor(ActorSystemContext context) {
+        super(context);
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        logger.debug("Received message: {}", msg);
+        if (msg instanceof StatsPersistMsg) {
+            try {
+                onStatsPersistMsg((StatsPersistMsg) msg);
+            } catch (Exception e) {
+                logger.warning("Failed to persist statistics: {}", msg, e);
+            }
+        }
+    }
+
+    public void onStatsPersistMsg(StatsPersistMsg msg) throws Exception {
+        Event event = new Event();
+        event.setEntityId(msg.getEntityId());
+        event.setTenantId(msg.getTenantId());
+        event.setType(DataConstants.STATS);
+        event.setBody(toBodyJson(systemContext.getDiscoveryService().getCurrentServer().getServerAddress(), msg.getMessagesProcessed(), msg.getErrorsOccurred()));
+        systemContext.getEventService().save(event);
+    }
+
+    private JsonNode toBodyJson(ServerAddress server, long messagesProcessed, long errorsOccurred) {
+        return mapper.createObjectNode().put("server", server.toString()).put("messagesProcessed", messagesProcessed).put("errorsOccurred", errorsOccurred);
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<StatsActor> {
+        private static final long serialVersionUID = 1L;
+
+        public ActorCreator(ActorSystemContext context) {
+            super(context);
+        }
+
+        @Override
+        public StatsActor create() throws Exception {
+            return new StatsActor(context);
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java
new file mode 100644
index 0000000..437ef96
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.stats;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+@AllArgsConstructor
+@Getter
+@ToString
+public final class StatsPersistMsg {
+    private long messagesProcessed;
+    private long errorsOccurred;
+    private TenantId tenantId;
+    private EntityId entityId;
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
new file mode 100644
index 0000000..61c9fc6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/stats/StatsPersistTick.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.stats;
+
+public final class StatsPersistTick {}
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java b/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java
new file mode 100644
index 0000000..7d41c7b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/RuleChainDeviceMsg.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.tenant;
+
+import org.thingsboard.server.actors.rule.RuleActorChain;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+
+public class RuleChainDeviceMsg {
+
+    private final ToDeviceActorMsg toDeviceActorMsg;
+    private final RuleActorChain ruleChain;
+
+    public RuleChainDeviceMsg(ToDeviceActorMsg toDeviceActorMsg, RuleActorChain ruleChain) {
+        super();
+        this.toDeviceActorMsg = toDeviceActorMsg;
+        this.ruleChain = ruleChain;
+    }
+
+    public ToDeviceActorMsg getToDeviceActorMsg() {
+        return toDeviceActorMsg;
+    }
+
+    public RuleActorChain getRuleChain() {
+        return ruleChain;
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
new file mode 100644
index 0000000..965c652
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
@@ -0,0 +1,184 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors.tenant;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.actors.device.DeviceActor;
+import org.thingsboard.server.actors.plugin.PluginTerminationMsg;
+import org.thingsboard.server.actors.rule.ComplexRuleActorChain;
+import org.thingsboard.server.actors.rule.RuleActorChain;
+import org.thingsboard.server.actors.service.ContextAwareActor;
+import org.thingsboard.server.actors.service.ContextBasedCreator;
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.actors.shared.plugin.PluginManager;
+import org.thingsboard.server.actors.shared.plugin.TenantPluginManager;
+import org.thingsboard.server.actors.shared.rule.RuleManager;
+import org.thingsboard.server.actors.shared.rule.TenantRuleManager;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ClusterEventMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.event.Logging;
+import akka.event.LoggingAdapter;
+import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
+
+public class TenantActor extends ContextAwareActor {
+
+    private final LoggingAdapter logger = Logging.getLogger(getContext().system(), this);
+
+    private final TenantId tenantId;
+    private final RuleManager ruleManager;
+    private final PluginManager pluginManager;
+    private final Map<DeviceId, ActorRef> deviceActors;
+
+    private TenantActor(ActorSystemContext systemContext, TenantId tenantId) {
+        super(systemContext);
+        this.tenantId = tenantId;
+        this.ruleManager = new TenantRuleManager(systemContext, tenantId);
+        this.pluginManager = new TenantPluginManager(systemContext, tenantId);
+        this.deviceActors = new HashMap<>();
+    }
+
+    @Override
+    public void preStart() {
+        logger.info("[{}] Starting tenant actor.", tenantId);
+        try {
+            ruleManager.init(this.context());
+            pluginManager.init(this.context());
+            logger.info("[{}] Tenant actor started.", tenantId);
+        } catch (Exception e) {
+            logger.error(e, "[{}] Unknown failure", tenantId);
+        }
+    }
+
+    @Override
+    public void onReceive(Object msg) throws Exception {
+        logger.debug("[{}] Received message: {}", tenantId, msg);
+        if (msg instanceof RuleChainDeviceMsg) {
+            process((RuleChainDeviceMsg) msg);
+        } else if (msg instanceof ToDeviceActorMsg) {
+            onToDeviceActorMsg((ToDeviceActorMsg) msg);
+        } else if (msg instanceof ToPluginActorMsg) {
+            onToPluginMsg((ToPluginActorMsg) msg);
+        } else if (msg instanceof ToRuleActorMsg) {
+            onToRuleMsg((ToRuleActorMsg) msg);
+        } else if (msg instanceof ToDeviceActorNotificationMsg) {
+            onToDeviceActorMsg((ToDeviceActorNotificationMsg) msg);
+        } else if (msg instanceof ClusterEventMsg) {
+            broadcast(msg);
+        } else if (msg instanceof ComponentLifecycleMsg) {
+            onComponentLifecycleMsg((ComponentLifecycleMsg) msg);
+        } else if (msg instanceof PluginTerminationMsg) {
+            onPluginTerminated((PluginTerminationMsg) msg);
+        } else {
+            logger.warning("[{}] Unknown message: {}!", tenantId, msg);
+        }
+    }
+
+    private void broadcast(Object msg) {
+        pluginManager.broadcast(msg);
+        deviceActors.values().stream().forEach(actorRef -> actorRef.tell(msg, ActorRef.noSender()));
+    }
+
+    private void onToDeviceActorMsg(ToDeviceActorMsg msg) {
+        getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+    }
+
+    private void onToDeviceActorMsg(ToDeviceActorNotificationMsg msg) {
+        getOrCreateDeviceActor(msg.getDeviceId()).tell(msg, ActorRef.noSender());
+    }
+
+    private void onToRuleMsg(ToRuleActorMsg msg) {
+        ActorRef target = ruleManager.getOrCreateRuleActor(this.context(), msg.getRuleId());
+        target.tell(msg, ActorRef.noSender());
+    }
+
+    private void onToPluginMsg(ToPluginActorMsg msg) {
+        if (msg.getPluginTenantId().equals(tenantId)) {
+            ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId());
+            pluginActor.tell(msg, ActorRef.noSender());
+        } else {
+            context().parent().tell(msg, ActorRef.noSender());
+        }
+    }
+
+    private void onComponentLifecycleMsg(ComponentLifecycleMsg msg) {
+        if (msg.getPluginId().isPresent()) {
+            ActorRef pluginActor = pluginManager.getOrCreatePluginActor(this.context(), msg.getPluginId().get());
+            pluginActor.tell(msg, ActorRef.noSender());
+        } else if (msg.getRuleId().isPresent()) {
+            ActorRef target;
+            Optional<ActorRef> ref = ruleManager.update(this.context(), msg.getRuleId().get(), msg.getEvent());
+            if (ref.isPresent()) {
+                target = ref.get();
+            } else {
+                logger.debug("Failed to find actor for rule: [{}]", msg.getRuleId());
+                return;
+            }
+            target.tell(msg, ActorRef.noSender());
+        } else {
+            logger.debug("[{}] Invalid component lifecycle msg.", tenantId);
+        }
+    }
+
+    private void onPluginTerminated(PluginTerminationMsg msg) {
+        pluginManager.remove(msg.getId());
+    }
+
+    private void process(RuleChainDeviceMsg msg) {
+        ToDeviceActorMsg toDeviceActorMsg = msg.getToDeviceActorMsg();
+        ActorRef deviceActor = getOrCreateDeviceActor(toDeviceActorMsg.getDeviceId());
+        RuleActorChain chain = new ComplexRuleActorChain(msg.getRuleChain(), ruleManager.getRuleChain());
+        deviceActor.tell(new RuleChainDeviceMsg(toDeviceActorMsg, chain), context().self());
+    }
+
+    private ActorRef getOrCreateDeviceActor(DeviceId deviceId) {
+        ActorRef deviceActor = deviceActors.get(deviceId);
+        if (deviceActor == null) {
+            deviceActor = context().actorOf(Props.create(new DeviceActor.ActorCreator(systemContext, tenantId, deviceId))
+                    .withDispatcher(DefaultActorService.CORE_DISPATCHER_NAME), deviceId.toString());
+            deviceActors.put(deviceId, deviceActor);
+        }
+        return deviceActor;
+    }
+
+    public static class ActorCreator extends ContextBasedCreator<TenantActor> {
+        private static final long serialVersionUID = 1L;
+
+        private final TenantId tenantId;
+
+        public ActorCreator(ActorSystemContext context, TenantId tenantId) {
+            super(context);
+            this.tenantId = tenantId;
+        }
+
+        @Override
+        public TenantActor create() throws Exception {
+            return new TenantActor(context, tenantId);
+        }
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java
new file mode 100644
index 0000000..5100bf8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/CurrentServerInstanceService.java
@@ -0,0 +1,55 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos;
+
+import javax.annotation.PostConstruct;
+
+import static org.thingsboard.server.utils.MiscUtils.missingProperty;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@Slf4j
+public class CurrentServerInstanceService implements ServerInstanceService {
+
+    @Value("${rpc.bind_host}")
+    private String rpcHost;
+    @Value("${rpc.bind_port}")
+    private Integer rpcPort;
+
+    private ServerInstance self;
+
+    @PostConstruct
+    public void init() {
+        Assert.hasLength(rpcHost, missingProperty("rpc.bind_host"));
+        Assert.notNull(rpcPort, missingProperty("rpc.bind_port"));
+
+        self = new ServerInstance(ServerInstanceProtos.ServerInfo.newBuilder().setHost(rpcHost).setPort(rpcPort).setTs(System.currentTimeMillis()).build());
+        log.info("Current server instance: [{};{}]", self.getHost(), self.getPort());
+    }
+
+    @Override
+    public ServerInstance getSelf() {
+        return self;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java
new file mode 100644
index 0000000..bd6fe7b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryService.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface DiscoveryService {
+
+    void publishCurrentServer();
+
+    void unpublishCurrentServer();
+
+    ServerInstance getCurrentServer();
+
+    List<ServerInstance> getOtherServers();
+
+    boolean addListener(DiscoveryServiceListener listener);
+
+    boolean removeListener(DiscoveryServiceListener listener);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java
new file mode 100644
index 0000000..a0b8eba
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DiscoveryServiceListener.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface DiscoveryServiceListener {
+
+    void onServerAdded(ServerInstance server);
+
+    void onServerUpdated(ServerInstance server);
+
+    void onServerRemoved(ServerInstance server);
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
new file mode 100644
index 0000000..fdf9fdd
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/DummyDiscoveryService.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.DependsOn;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.service.environment.EnvironmentLogService;
+
+import javax.annotation.PostConstruct;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "false", matchIfMissing = true)
+@Slf4j
+@DependsOn("environmentLogService")
+public class DummyDiscoveryService implements DiscoveryService {
+
+    @Autowired
+    private ServerInstanceService serverInstance;
+
+    @PostConstruct
+    public void init() {
+        log.info("Initializing...");
+    }
+
+    @Override
+    public void publishCurrentServer() {
+
+    }
+
+    @Override
+    public void unpublishCurrentServer() {
+
+    }
+
+    @Override
+    public ServerInstance getCurrentServer() {
+        return serverInstance.getSelf();
+    }
+
+    @Override
+    public List<ServerInstance> getOtherServers() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public boolean addListener(DiscoveryServiceListener listener) {
+        return false;
+    }
+
+    @Override
+    public boolean removeListener(DiscoveryServiceListener listener) {
+        return false;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
new file mode 100644
index 0000000..5f9d31b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstance.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+import lombok.AccessLevel;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+@EqualsAndHashCode(exclude = {"serverInfo", "serverAddress"})
+public final class ServerInstance implements Comparable<ServerInstance> {
+
+    @Getter(AccessLevel.PACKAGE)
+    private final ServerInfo serverInfo;
+    @Getter
+    private final String host;
+    @Getter
+    private final int port;
+    @Getter
+    private final ServerAddress serverAddress;
+
+    public ServerInstance(ServerInfo serverInfo) {
+        this.serverInfo = serverInfo;
+        this.host = serverInfo.getHost();
+        this.port = serverInfo.getPort();
+        this.serverAddress = new ServerAddress(host, port);
+    }
+
+    @Override
+    public int compareTo(ServerInstance o) {
+        return this.serverAddress.compareTo(o.serverAddress);
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java
new file mode 100644
index 0000000..af023ed
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ServerInstanceService.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ServerInstanceService {
+
+    ServerInstance getSelf();
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
new file mode 100644
index 0000000..29e9b3c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/discovery/ZkDiscoveryService.java
@@ -0,0 +1,207 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.discovery;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.curator.framework.CuratorFramework;
+import org.apache.curator.framework.CuratorFrameworkFactory;
+import org.apache.curator.framework.recipes.cache.ChildData;
+import org.apache.curator.framework.recipes.cache.PathChildrenCache;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
+import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
+import org.apache.curator.retry.RetryForever;
+import org.apache.curator.utils.CloseableUtils;
+import org.apache.zookeeper.CreateMode;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos.ServerInfo;
+import org.thingsboard.server.utils.MiscUtils;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@ConditionalOnProperty(prefix = "zk", value = "enabled", havingValue = "true", matchIfMissing = false)
+@Slf4j
+public class ZkDiscoveryService implements DiscoveryService, PathChildrenCacheListener, ApplicationListener<ApplicationReadyEvent> {
+
+    @Value("${zk.url}")
+    private String zkUrl;
+    @Value("${zk.retry_interval_ms}")
+    private Integer zkRetryInterval;
+    @Value("${zk.connection_timeout_ms}")
+    private Integer zkConnectionTimeout;
+    @Value("${zk.session_timeout_ms}")
+    private Integer zkSessionTimeout;
+    @Value("${zk.zk_dir}")
+    private String zkDir;
+
+    private String zkNodesDir;
+
+    @Autowired
+    private ServerInstanceService serverInstance;
+
+    private final List<DiscoveryServiceListener> listeners = new CopyOnWriteArrayList<>();
+
+    private CuratorFramework client;
+    private PathChildrenCache cache;
+    private String nodePath;
+
+
+    @PostConstruct
+    public void init() {
+        log.info("Initializing...");
+        Assert.hasLength(zkUrl, MiscUtils.missingProperty("zk.url"));
+        Assert.notNull(zkRetryInterval, MiscUtils.missingProperty("zk.retry_interval_ms"));
+        Assert.notNull(zkConnectionTimeout, MiscUtils.missingProperty("zk.connection_timeout_ms"));
+        Assert.notNull(zkSessionTimeout, MiscUtils.missingProperty("zk.session_timeout_ms"));
+
+        log.info("Initializing discovery service using ZK connect string: {}", zkUrl);
+
+        zkNodesDir = zkDir + "/nodes";
+        try {
+            client = CuratorFrameworkFactory.newClient(zkUrl, zkSessionTimeout, zkConnectionTimeout, new RetryForever(zkRetryInterval));
+            client.start();
+            client.blockUntilConnected();
+            cache = new PathChildrenCache(client, zkNodesDir, true);
+            cache.getListenable().addListener(this);
+            cache.start();
+        } catch (Exception e) {
+            log.error("Failed to connect to ZK: {}", e.getMessage(), e);
+            CloseableUtils.closeQuietly(client);
+            throw new RuntimeException(e);
+        }
+    }
+
+    @PreDestroy
+    public void destroy() {
+        unpublishCurrentServer();
+        CloseableUtils.closeQuietly(client);
+        log.info("Stopped discovery service");
+    }
+
+    @Override
+    public void publishCurrentServer() {
+        try {
+            ServerInstance self = this.serverInstance.getSelf();
+            log.info("[{}:{}] Creating ZK node for current instance", self.getHost(), self.getPort());
+            nodePath = client.create()
+                    .creatingParentsIfNeeded()
+                    .withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(zkNodesDir + "/", self.getServerInfo().toByteArray());
+            log.info("[{}:{}] Created ZK node for current instance: {}", self.getHost(), self.getPort(), nodePath);
+        } catch (Exception e) {
+            log.error("Failed to create ZK node", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public void unpublishCurrentServer() {
+        try {
+            if (nodePath != null) {
+                client.delete().forPath(nodePath);
+            }
+        } catch (Exception e) {
+            log.error("Failed to delete ZK node {}", nodePath, e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    public ServerInstance getCurrentServer() {
+        return serverInstance.getSelf();
+    }
+
+    @Override
+    public List<ServerInstance> getOtherServers() {
+        return cache.getCurrentData().stream()
+                .filter(cd -> !cd.getPath().equals(nodePath))
+                .map(cd -> {
+                    try {
+                        return new ServerInstance(ServerInfo.parseFrom(cd.getData()));
+                    } catch (InvalidProtocolBufferException e) {
+                        log.error("Failed to decode ZK node", e);
+                        throw new RuntimeException(e);
+                    }
+                })
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean addListener(DiscoveryServiceListener listener) {
+        return listeners.add(listener);
+    }
+
+    @Override
+    public boolean removeListener(DiscoveryServiceListener listener) {
+        return listeners.remove(listener);
+    }
+
+    @Override
+    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
+        publishCurrentServer();
+        getOtherServers().stream().forEach(
+                server -> log.info("Found active server: [{}:{}]", server.getHost(), server.getPort())
+        );
+    }
+
+    @Override
+    public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
+        ChildData data = pathChildrenCacheEvent.getData();
+        if (data == null) {
+            log.debug("Ignoring {} due to empty child data", pathChildrenCacheEvent);
+            return;
+        } else if (data.getData() == null) {
+            log.debug("Ignoring {} due to empty child's data", pathChildrenCacheEvent);
+            return;
+        } else if (nodePath != null && nodePath.equals(data.getPath())) {
+            log.debug("Ignoring event about current server {}", pathChildrenCacheEvent);
+            return;
+        }
+        ServerInstance instance;
+        try {
+            instance = new ServerInstance(ServerInfo.parseFrom(data.getData()));
+        } catch (IOException e) {
+            log.error("Failed to decode server instance for node {}", data.getPath(), e);
+            throw e;
+        }
+        log.info("Processing [{}] event for [{}:{}]", pathChildrenCacheEvent.getType(), instance.getHost(), instance.getPort());
+        switch (pathChildrenCacheEvent.getType()) {
+            case CHILD_ADDED:
+                listeners.stream().forEach(listener -> listener.onServerAdded(instance));
+                break;
+            case CHILD_UPDATED:
+                listeners.stream().forEach(listener -> listener.onServerUpdated(instance));
+                break;
+            case CHILD_REMOVED:
+                listeners.stream().forEach(listener -> listener.onServerRemoved(instance));
+                break;
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
new file mode 100644
index 0000000..352d89e
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ClusterRoutingService.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.routing;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ClusterRoutingService {
+
+    ServerAddress getCurrentServer();
+
+    Optional<ServerAddress> resolve(UUIDBased entityId);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
new file mode 100644
index 0000000..3c9ecf8
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/routing/ConsistentClusterRoutingService.java
@@ -0,0 +1,142 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.routing;
+
+import com.google.common.hash.HashCode;
+import com.google.common.hash.HashFunction;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.Assert;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryServiceListener;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+import org.thingsboard.server.utils.MiscUtils;
+
+import javax.annotation.PostConstruct;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentNavigableMap;
+import java.util.concurrent.ConcurrentSkipListMap;
+
+/**
+ * Cluster service implementation based on consistent hash ring
+ */
+
+@Service
+@Slf4j
+public class ConsistentClusterRoutingService implements ClusterRoutingService, DiscoveryServiceListener {
+
+    @Autowired
+    private DiscoveryService discoveryService;
+
+    @Value("${cluster.hash_function_name}")
+    private String hashFunctionName;
+    @Value("${cluster.vitrual_nodes_size}")
+    private Integer virtualNodesSize;
+
+    private ServerInstance currentServer;
+
+    private HashFunction hashFunction;
+
+    private final ConcurrentNavigableMap<Long, ServerInstance> circle =
+            new ConcurrentSkipListMap<>();
+
+    @PostConstruct
+    public void init() {
+        log.info("Initializing Cluster routing service!");
+        hashFunction = MiscUtils.forName(hashFunctionName);
+        discoveryService.addListener(this);
+        this.currentServer = discoveryService.getCurrentServer();
+        addNode(discoveryService.getCurrentServer());
+        for (ServerInstance instance : discoveryService.getOtherServers()) {
+            addNode(instance);
+        }
+        logCircle();
+        log.info("Cluster routing service initialized!");
+    }
+
+    @Override
+    public ServerAddress getCurrentServer() {
+        return discoveryService.getCurrentServer().getServerAddress();
+    }
+
+    @Override
+    public Optional<ServerAddress> resolve(UUIDBased entityId) {
+        Assert.notNull(entityId);
+        if (circle.isEmpty()) {
+            return Optional.empty();
+        }
+        Long hash = hashFunction.newHasher().putLong(entityId.getId().getMostSignificantBits())
+                .putLong(entityId.getId().getLeastSignificantBits()).hash().asLong();
+        if (!circle.containsKey(hash)) {
+            ConcurrentNavigableMap<Long, ServerInstance> tailMap =
+                    circle.tailMap(hash);
+            hash = tailMap.isEmpty() ?
+                    circle.firstKey() : tailMap.firstKey();
+        }
+        ServerInstance result = circle.get(hash);
+        if (!currentServer.equals(result)) {
+            return Optional.of(result.getServerAddress());
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public void onServerAdded(ServerInstance server) {
+        log.debug("On server added event: {}", server);
+        addNode(server);
+        logCircle();
+    }
+
+    @Override
+    public void onServerUpdated(ServerInstance server) {
+        log.debug("Ignoring server onUpdate event: {}", server);
+    }
+
+    @Override
+    public void onServerRemoved(ServerInstance server) {
+        log.debug("On server removed event: {}", server);
+        removeNode(server);
+        logCircle();
+    }
+
+    private void addNode(ServerInstance instance) {
+        for (int i = 0; i < virtualNodesSize; i++) {
+            circle.put(hash(instance, i).asLong(), instance);
+        }
+    }
+
+    private void removeNode(ServerInstance instance) {
+        for (int i = 0; i < virtualNodesSize; i++) {
+            circle.remove(hash(instance, i).asLong());
+        }
+    }
+
+    private HashCode hash(ServerInstance instance, int i) {
+        return hashFunction.newHasher().putString(instance.getHost(), MiscUtils.UTF8).putInt(instance.getPort()).putInt(i).hash();
+    }
+
+    private void logCircle() {
+        log.trace("Consistent Hash Circle Start");
+        circle.entrySet().stream().forEach((e) -> log.debug("{} -> {}", e.getKey(), e.getValue().getServerAddress()));
+        log.trace("Consistent Hash Circle End");
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
new file mode 100644
index 0000000..3755c0b
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterGrpcService.java
@@ -0,0 +1,273 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.rpc;
+
+import com.google.protobuf.ByteString;
+import io.grpc.Server;
+import io.grpc.ServerBuilder;
+import io.grpc.stub.StreamObserver;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.util.SerializationUtils;
+import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
+import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
+import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
+import org.thingsboard.server.actors.service.ActorService;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+import org.thingsboard.server.gen.cluster.ClusterRpcServiceGrpc;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+import org.thingsboard.server.service.cluster.discovery.ServerInstanceService;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.io.IOException;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service
+@Slf4j
+public class ClusterGrpcService extends ClusterRpcServiceGrpc.ClusterRpcServiceImplBase implements ClusterRpcService {
+
+    @Autowired
+    private ServerInstanceService instanceService;
+
+    private RpcMsgListener listener;
+
+    private Server server;
+
+    private ServerInstance instance;
+
+    private ConcurrentMap<UUID, RpcSessionCreationFuture> pendingSessionMap = new ConcurrentHashMap<>();
+
+    public void init(RpcMsgListener listener) {
+        this.listener = listener;
+        log.info("Initializing RPC service!");
+        instance = instanceService.getSelf();
+        server = ServerBuilder.forPort(instance.getPort()).addService(this).build();
+        log.info("Going to start RPC server using port: {}", instance.getPort());
+        try {
+            server.start();
+        } catch (IOException e) {
+            log.error("Failed to start RPC server!", e);
+            throw new RuntimeException("Failed to start RPC server!");
+        }
+        log.info("RPC service initialized!");
+    }
+
+    @Override
+    public void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> msg) {
+        RpcSessionCreationFuture future = pendingSessionMap.remove(msgUid);
+        if (future != null) {
+            try {
+                future.onMsg(msg);
+            } catch (InterruptedException e) {
+                log.warn("Failed to report created session!");
+            }
+        } else {
+            log.warn("Failed to lookup pending session!");
+        }
+    }
+
+    @Override
+    public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> handlePluginMsgs(StreamObserver<ClusterAPIProtos.ToRpcServerMessage> responseObserver) {
+        log.info("Processing new session.");
+        return createSession(new RpcSessionCreateRequestMsg(UUID.randomUUID(), null, responseObserver));
+    }
+
+    @PreDestroy
+    public void stop() {
+        if (server != null) {
+            log.info("Going to onStop RPC server");
+            server.shutdownNow();
+            try {
+                server.awaitTermination();
+                log.info("RPC server stopped!");
+            } catch (InterruptedException e) {
+                log.warn("Failed to onStop RPC server!");
+            }
+        }
+    }
+
+    @Override
+    public void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToDeviceActorRpcMsg(toProtoMsg(toForward)).build();
+        tell(serverAddress, msg);
+    }
+
+    @Override
+    public void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToDeviceActorNotificationRpcMsg(toProtoMsg(toForward)).build();
+        tell(serverAddress, msg);
+    }
+
+    @Override
+    public void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToDeviceRpcRequestRpcMsg(toProtoMsg(toForward)).build();
+        tell(serverAddress, msg);
+    }
+
+    @Override
+    public void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToPluginRpcResponseRpcMsg(toProtoMsg(toForward)).build();
+        tell(serverAddress, msg);
+    }
+
+    @Override
+    public void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToDeviceSessionActorRpcMsg(toProtoMsg(toForward)).build();
+        tell(serverAddress, msg);
+    }
+
+    @Override
+    public void tell(PluginRpcMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToPluginRpcMsg(toProtoMsg(toForward)).build();
+        tell(toForward.getRpcMsg().getServerAddress(), msg);
+    }
+
+    @Override
+    public void broadcast(ToAllNodesMsg toForward) {
+        ClusterAPIProtos.ToRpcServerMessage msg = ClusterAPIProtos.ToRpcServerMessage.newBuilder()
+                .setToAllNodesRpcMsg(toProtoMsg(toForward)).build();
+        listener.onMsg(new RpcBroadcastMsg(msg));
+    }
+
+    private void tell(ServerAddress serverAddress, ClusterAPIProtos.ToRpcServerMessage msg) {
+        listener.onMsg(new RpcSessionTellMsg(serverAddress, msg));
+    }
+
+    private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> createSession(RpcSessionCreateRequestMsg msg) {
+        RpcSessionCreationFuture future = new RpcSessionCreationFuture();
+        pendingSessionMap.put(msg.getMsgUid(), future);
+        listener.onMsg(msg);
+        try {
+            StreamObserver<ClusterAPIProtos.ToRpcServerMessage> observer = future.get();
+            log.info("Processed new session.");
+            return observer;
+        } catch (Exception e) {
+            log.info("Failed to process session.", e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static ClusterAPIProtos.ToDeviceActorRpcMessage toProtoMsg(ToDeviceActorMsg msg) {
+        return ClusterAPIProtos.ToDeviceActorRpcMessage.newBuilder().setData(
+                ByteString.copyFrom(SerializationUtils.serialize(msg))
+        ).build();
+    }
+
+    private static ClusterAPIProtos.ToDeviceActorNotificationRpcMessage toProtoMsg(ToDeviceActorNotificationMsg msg) {
+        return ClusterAPIProtos.ToDeviceActorNotificationRpcMessage.newBuilder().setData(
+                ByteString.copyFrom(SerializationUtils.serialize(msg))
+        ).build();
+    }
+
+    private static ClusterAPIProtos.ToDeviceRpcRequestRpcMessage toProtoMsg(ToDeviceRpcRequestPluginMsg msg) {
+        ClusterAPIProtos.ToDeviceRpcRequestRpcMessage.Builder builder = ClusterAPIProtos.ToDeviceRpcRequestRpcMessage.newBuilder();
+        ToDeviceRpcRequest request = msg.getMsg();
+
+        builder.setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
+                .setTenantId(toUid(msg.getPluginTenantId().getId()))
+                .setPluginId(toUid(msg.getPluginId().getId()))
+                .build());
+
+        builder.setDeviceTenantId(toUid(msg.getTenantId()));
+        builder.setDeviceId(toUid(msg.getDeviceId()));
+
+        builder.setMsgId(toUid(request.getId()));
+        builder.setOneway(request.isOneway());
+        builder.setExpTime(request.getExpirationTime());
+        builder.setMethod(request.getBody().getMethod());
+        builder.setParams(request.getBody().getParams());
+
+        return builder.build();
+    }
+
+    private static ClusterAPIProtos.ToPluginRpcResponseRpcMessage toProtoMsg(ToPluginRpcResponseDeviceMsg msg) {
+        ClusterAPIProtos.ToPluginRpcResponseRpcMessage.Builder builder = ClusterAPIProtos.ToPluginRpcResponseRpcMessage.newBuilder();
+        FromDeviceRpcResponse request = msg.getResponse();
+
+        builder.setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
+                .setTenantId(toUid(msg.getPluginTenantId().getId()))
+                .setPluginId(toUid(msg.getPluginId().getId()))
+                .build());
+
+        builder.setMsgId(toUid(request.getId()));
+        request.getResponse().ifPresent(builder::setResponse);
+        request.getError().ifPresent(e -> builder.setError(e.name()));
+
+        return builder.build();
+    }
+
+    private ClusterAPIProtos.ToAllNodesRpcMessage toProtoMsg(ToAllNodesMsg msg) {
+        return ClusterAPIProtos.ToAllNodesRpcMessage.newBuilder().setData(
+                ByteString.copyFrom(SerializationUtils.serialize(msg))
+        ).build();
+    }
+
+
+    private ClusterAPIProtos.ToPluginRpcMessage toProtoMsg(PluginRpcMsg msg) {
+        return ClusterAPIProtos.ToPluginRpcMessage.newBuilder()
+                .setClazz(msg.getRpcMsg().getMsgClazz())
+                .setData(ByteString.copyFrom(msg.getRpcMsg().getMsgData()))
+                .setAddress(ClusterAPIProtos.PluginAddress.newBuilder()
+                        .setTenantId(toUid(msg.getPluginTenantId().getId()))
+                        .setPluginId(toUid(msg.getPluginId().getId()))
+                        .build()
+                ).build();
+    }
+
+    private static ClusterAPIProtos.Uid toUid(EntityId uuid) {
+        return toUid(uuid.getId());
+    }
+
+    private static ClusterAPIProtos.Uid toUid(UUID uuid) {
+        return ClusterAPIProtos.Uid.newBuilder().setPluginUuidMsb(uuid.getMostSignificantBits()).setPluginUuidLsb(
+                uuid.getLeastSignificantBits()).build();
+    }
+
+    private static ClusterAPIProtos.ToDeviceSessionActorRpcMessage toProtoMsg(ToDeviceSessionActorMsg msg) {
+        return ClusterAPIProtos.ToDeviceSessionActorRpcMessage.newBuilder().setData(
+                ByteString.copyFrom(SerializationUtils.serialize(msg))
+        ).build();
+    }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
new file mode 100644
index 0000000..0f13164
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/ClusterRpcService.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.rpc;
+
+import io.grpc.stub.StreamObserver;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginRpcResponseDeviceMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ClusterRpcService {
+
+    void init(RpcMsgListener listener);
+
+    void tell(ServerAddress serverAddress, ToDeviceActorMsg toForward);
+
+    void tell(ServerAddress serverAddress, ToDeviceSessionActorMsg toForward);
+
+    void tell(ServerAddress serverAddress, ToDeviceActorNotificationMsg toForward);
+
+    void tell(ServerAddress serverAddress, ToDeviceRpcRequestPluginMsg toForward);
+
+    void tell(ServerAddress serverAddress, ToPluginRpcResponseDeviceMsg toForward);
+
+    void tell(PluginRpcMsg toForward);
+
+    void broadcast(ToAllNodesMsg msg);
+
+    void onSessionCreated(UUID msgUid, StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream);
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
new file mode 100644
index 0000000..0f53839
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSession.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.rpc;
+
+import io.grpc.stub.StreamObserver;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.io.Closeable;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@Slf4j
+final public class GrpcSession implements Closeable {
+    private final UUID sessionId;
+    private final boolean client;
+    private final GrpcSessionListener listener;
+    private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> inputStream;
+    private StreamObserver<ClusterAPIProtos.ToRpcServerMessage> outputStream;
+
+    private boolean connected;
+    private ServerAddress remoteServer;
+
+    public GrpcSession(GrpcSessionListener listener) {
+        this(null, listener);
+    }
+
+    public GrpcSession(ServerAddress remoteServer, GrpcSessionListener listener) {
+        this.sessionId = UUID.randomUUID();
+        this.listener = listener;
+        if (remoteServer != null) {
+            this.client = true;
+            this.connected = true;
+            this.remoteServer = remoteServer;
+        } else {
+            this.client = false;
+        }
+    }
+
+    public void initInputStream() {
+        this.inputStream = new StreamObserver<ClusterAPIProtos.ToRpcServerMessage>() {
+            @Override
+            public void onNext(ClusterAPIProtos.ToRpcServerMessage msg) {
+                if (!connected) {
+                    if (msg.hasConnectMsg()) {
+                        connected = true;
+                        ClusterAPIProtos.ServerAddress rpcAddress = msg.getConnectMsg().getServerAddress();
+                        remoteServer = new ServerAddress(rpcAddress.getHost(), rpcAddress.getPort());
+                        listener.onConnected(GrpcSession.this);
+                    }
+                }
+                if (connected) {
+                    if (msg.hasToPluginRpcMsg()) {
+                        listener.onToPluginRpcMsg(GrpcSession.this, msg.getToPluginRpcMsg());
+                    }
+                    if (msg.hasToDeviceActorRpcMsg()) {
+                        listener.onToDeviceActorRpcMsg(GrpcSession.this, msg.getToDeviceActorRpcMsg());
+                    }
+                    if (msg.hasToDeviceSessionActorRpcMsg()) {
+                        listener.onToDeviceSessionActorRpcMsg(GrpcSession.this, msg.getToDeviceSessionActorRpcMsg());
+                    }
+                    if (msg.hasToDeviceActorNotificationRpcMsg()) {
+                        listener.onToDeviceActorNotificationRpcMsg(GrpcSession.this, msg.getToDeviceActorNotificationRpcMsg());
+                    }
+                    if (msg.hasToDeviceRpcRequestRpcMsg()) {
+                        listener.onToDeviceRpcRequestRpcMsg(GrpcSession.this, msg.getToDeviceRpcRequestRpcMsg());
+                    }
+                    if (msg.hasToPluginRpcResponseRpcMsg()) {
+                        listener.onFromDeviceRpcResponseRpcMsg(GrpcSession.this, msg.getToPluginRpcResponseRpcMsg());
+                    }
+                    if (msg.hasToAllNodesRpcMsg()) {
+                        listener.onToAllNodesRpcMessage(GrpcSession.this, msg.getToAllNodesRpcMsg());
+                    }
+                }
+            }
+
+            @Override
+            public void onError(Throwable t) {
+                listener.onError(GrpcSession.this, t);
+            }
+
+            @Override
+            public void onCompleted() {
+                outputStream.onCompleted();
+                listener.onDisconnected(GrpcSession.this);
+            }
+        };
+    }
+
+    public void initOutputStream() {
+        if (client) {
+            listener.onConnected(GrpcSession.this);
+        }
+    }
+
+    public void sendMsg(ClusterAPIProtos.ToRpcServerMessage msg) {
+        outputStream.onNext(msg);
+    }
+
+    public void onError(Throwable t) {
+        outputStream.onError(t);
+    }
+
+    @Override
+    public void close() {
+        try {
+            outputStream.onCompleted();
+        } catch (IllegalStateException e) {
+            log.debug("[{}] Failed to close output stream: {}", sessionId, e.getMessage());
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
new file mode 100644
index 0000000..f80b394
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/GrpcSessionListener.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.rpc;
+
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface GrpcSessionListener {
+
+    void onConnected(GrpcSession session);
+
+    void onDisconnected(GrpcSession session);
+
+    void onToPluginRpcMsg(GrpcSession session, ClusterAPIProtos.ToPluginRpcMessage msg);
+
+    void onToDeviceActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceActorRpcMessage msg);
+
+    void onToDeviceActorNotificationRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToDeviceActorNotificationRpcMessage msg);
+
+    void onToDeviceSessionActorRpcMsg(GrpcSession session, ClusterAPIProtos.ToDeviceSessionActorRpcMessage msg);
+
+    void onToAllNodesRpcMessage(GrpcSession grpcSession, ClusterAPIProtos.ToAllNodesRpcMessage toAllNodesRpcMessage);
+
+    void onToDeviceRpcRequestRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToDeviceRpcRequestRpcMessage toDeviceRpcRequestRpcMsg);
+
+    void onFromDeviceRpcResponseRpcMsg(GrpcSession grpcSession, ClusterAPIProtos.ToPluginRpcResponseRpcMessage toPluginRpcResponseRpcMsg);
+
+    void onError(GrpcSession session, Throwable t);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
new file mode 100644
index 0000000..982803a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcMsgListener.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.rpc;
+
+import org.thingsboard.server.actors.rpc.RpcBroadcastMsg;
+import org.thingsboard.server.actors.rpc.RpcSessionCreateRequestMsg;
+import org.thingsboard.server.actors.rpc.RpcSessionTellMsg;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+import org.thingsboard.server.common.msg.core.ToDeviceSessionActorMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.PluginRpcMsg;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RpcMsgListener {
+
+    void onMsg(ToDeviceActorMsg msg);
+
+    void onMsg(ToDeviceActorNotificationMsg msg);
+
+    void onMsg(ToDeviceSessionActorMsg msg);
+
+    void onMsg(ToAllNodesMsg nodeMsg);
+
+    void onMsg(ToPluginActorMsg msg);
+
+    void onMsg(RpcSessionCreateRequestMsg msg);
+
+    void onMsg(RpcSessionTellMsg rpcSessionTellMsg);
+
+    void onMsg(RpcBroadcastMsg rpcBroadcastMsg);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java
new file mode 100644
index 0000000..4966553
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/cluster/rpc/RpcSessionCreationFuture.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.cluster.rpc;
+
+import io.grpc.stub.StreamObserver;
+import org.thingsboard.server.gen.cluster.ClusterAPIProtos;
+
+import java.util.concurrent.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcSessionCreationFuture implements Future<StreamObserver<ClusterAPIProtos.ToRpcServerMessage>> {
+
+    private final BlockingQueue<StreamObserver<ClusterAPIProtos.ToRpcServerMessage>> queue = new ArrayBlockingQueue<>(1);
+
+    public void onMsg(StreamObserver<ClusterAPIProtos.ToRpcServerMessage> result) throws InterruptedException {
+        queue.put(result);
+    }
+
+    @Override
+    public boolean cancel(boolean mayInterruptIfRunning) {
+        return false;
+    }
+
+    @Override
+    public boolean isCancelled() {
+        return false;
+    }
+
+    @Override
+    public boolean isDone() {
+        return false;
+    }
+
+    @Override
+    public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> get() throws InterruptedException, ExecutionException {
+        return this.queue.take();
+    }
+
+    @Override
+    public StreamObserver<ClusterAPIProtos.ToRpcServerMessage> get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
+        StreamObserver<ClusterAPIProtos.ToRpcServerMessage> result = this.queue.poll(timeout, unit);
+        if (result == null) {
+            throw new TimeoutException();
+        } else {
+            return result;
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
new file mode 100644
index 0000000..a51464c
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
@@ -0,0 +1,190 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.component;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.dao.component.ComponentDescriptorService;
+import org.thingsboard.server.extensions.api.component.*;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@Slf4j
+public class AnnotationComponentDiscoveryService implements ComponentDiscoveryService {
+
+    @Value("${plugins.scan_packages}")
+    private String[] scanPackages;
+
+    @Autowired
+    private ComponentDescriptorService componentDescriptorService;
+
+    private Map<String, ComponentDescriptor> components = new HashMap<>();
+
+    private Map<ComponentType, List<ComponentDescriptor>> componentsMap = new HashMap<>();
+
+    private ObjectMapper mapper = new ObjectMapper();
+
+    @PostConstruct
+    public void init() {
+        registerComponents(ComponentType.FILTER, Filter.class);
+
+        registerComponents(ComponentType.PROCESSOR, Processor.class);
+
+        registerComponents(ComponentType.ACTION, Action.class);
+
+        registerComponents(ComponentType.PLUGIN, Plugin.class);
+
+        log.info("Found following definitions: {}", components.values());
+    }
+
+    private void registerComponents(ComponentType type, Class<? extends Annotation> annotation) {
+        List<ComponentDescriptor> components = persist(getBeanDefinitions(annotation), type);
+        componentsMap.put(type, components);
+        registerComponents(components);
+    }
+
+    private void registerComponents(Collection<ComponentDescriptor> comps) {
+        comps.stream().forEach(c -> components.put(c.getClazz(), c));
+    }
+
+    private List<ComponentDescriptor> persist(Set<BeanDefinition> filterDefs, ComponentType type) {
+        List<ComponentDescriptor> result = new ArrayList<>();
+        for (BeanDefinition def : filterDefs) {
+            ComponentDescriptor scannedComponent = new ComponentDescriptor();
+            String clazzName = def.getBeanClassName();
+            try {
+                scannedComponent.setType(type);
+                Class<?> clazz = Class.forName(clazzName);
+                String descriptorResourceName;
+                switch (type) {
+                    case FILTER:
+                        Filter filterAnnotation = clazz.getAnnotation(Filter.class);
+                        scannedComponent.setName(filterAnnotation.name());
+                        scannedComponent.setScope(filterAnnotation.scope());
+                        descriptorResourceName = filterAnnotation.descriptor();
+                        break;
+                    case PROCESSOR:
+                        Processor processorAnnotation = clazz.getAnnotation(Processor.class);
+                        scannedComponent.setName(processorAnnotation.name());
+                        scannedComponent.setScope(processorAnnotation.scope());
+                        descriptorResourceName = processorAnnotation.descriptor();
+                        break;
+                    case ACTION:
+                        Action actionAnnotation = clazz.getAnnotation(Action.class);
+                        scannedComponent.setName(actionAnnotation.name());
+                        scannedComponent.setScope(actionAnnotation.scope());
+                        descriptorResourceName = actionAnnotation.descriptor();
+                        break;
+                    case PLUGIN:
+                        Plugin pluginAnnotation = clazz.getAnnotation(Plugin.class);
+                        scannedComponent.setName(pluginAnnotation.name());
+                        scannedComponent.setScope(pluginAnnotation.scope());
+                        descriptorResourceName = pluginAnnotation.descriptor();
+                        for (Class<?> actionClazz : pluginAnnotation.actions()) {
+                            ComponentDescriptor actionComponent = getComponent(actionClazz.getName())
+                                    .orElseThrow(() -> {
+                                        log.error("Can't initialize plugin {}, due to missing action {}!", def.getBeanClassName(), actionClazz.getName());
+                                        return new ClassNotFoundException("Action: " + actionClazz.getName() + "is missing!");
+                                    });
+                            if (actionComponent.getType() != ComponentType.ACTION) {
+                                log.error("Plugin {} action {} has wrong component type!", def.getBeanClassName(), actionClazz.getName(), actionComponent.getType());
+                                throw new RuntimeException("Plugin " + def.getBeanClassName() + "action " + actionClazz.getName() + " has wrong component type!");
+                            }
+                        }
+                        scannedComponent.setActions(Arrays.asList(pluginAnnotation.actions()).stream().map(action -> action.getName()).collect(Collectors.joining(",")));
+                        break;
+                    default:
+                        throw new RuntimeException(type + " is not supported yet!");
+                }
+                scannedComponent.setConfigurationDescriptor(mapper.readTree(
+                        Resources.toString(Resources.getResource(descriptorResourceName), Charsets.UTF_8)));
+                scannedComponent.setClazz(clazzName);
+                log.info("Processing scanned component: {}", scannedComponent);
+            } catch (Exception e) {
+                log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
+                throw new RuntimeException(e);
+            }
+            ComponentDescriptor persistedComponent = componentDescriptorService.findByClazz(clazzName);
+            if (persistedComponent == null) {
+                log.info("Persisting new component: {}", scannedComponent);
+                scannedComponent = componentDescriptorService.saveComponent(scannedComponent);
+            } else if (scannedComponent.equals(persistedComponent)) {
+                log.info("Component is already persisted: {}", persistedComponent);
+                scannedComponent = persistedComponent;
+            } else {
+                log.info("Component {} will be updated to {}", persistedComponent, scannedComponent);
+                componentDescriptorService.deleteByClazz(persistedComponent.getClazz());
+                scannedComponent.setId(persistedComponent.getId());
+                scannedComponent = componentDescriptorService.saveComponent(scannedComponent);
+            }
+            result.add(scannedComponent);
+        }
+        return result;
+    }
+
+    private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
+        ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
+        scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
+        Set<BeanDefinition> defs = new HashSet<>();
+        for (String scanPackage : scanPackages) {
+            defs.addAll(scanner.findCandidateComponents(scanPackage));
+        }
+        return defs;
+    }
+
+    @Override
+    public List<ComponentDescriptor> getComponents(ComponentType type) {
+        return Collections.unmodifiableList(componentsMap.get(type));
+    }
+
+    @Override
+    public Optional<ComponentDescriptor> getComponent(String clazz) {
+        return Optional.ofNullable(components.get(clazz));
+    }
+
+    @Override
+    public List<ComponentDescriptor> getPluginActions(String pluginClazz) {
+        Optional<ComponentDescriptor> pluginOpt = getComponent(pluginClazz);
+        if (pluginOpt.isPresent()) {
+            ComponentDescriptor plugin = pluginOpt.get();
+            if (ComponentType.PLUGIN != plugin.getType()) {
+                throw new IllegalArgumentException(pluginClazz + " is not a plugin!");
+            }
+            List<ComponentDescriptor> result = new ArrayList<>();
+            for (String action : plugin.getActions().split(",")) {
+                getComponent(action).ifPresent(v -> result.add(v));
+            }
+            return result;
+        } else {
+            throw new IllegalArgumentException(pluginClazz + " is not a component!");
+        }
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
new file mode 100644
index 0000000..d14a60f
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ComponentDiscoveryService {
+
+    List<ComponentDescriptor> getComponents(ComponentType type);
+
+    Optional<ComponentDescriptor> getComponent(String clazz);
+
+    List<ComponentDescriptor> getPluginActions(String pluginClazz);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
new file mode 100644
index 0000000..f082824
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/ThingsboardServerApplication.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+import java.util.Arrays;
+
+@EnableAutoConfiguration
+@SpringBootApplication
+@ComponentScan({"org.thingsboard.server"})
+public class ThingsboardServerApplication {
+
+    private static final String SPRING_CONFIG_NAME_KEY = "--spring.config.name";
+    private static final String DEFAULT_SPRING_CONFIG_PARAM = SPRING_CONFIG_NAME_KEY + "=" + "thingsboard";
+
+    public static void main(String[] args) {
+        SpringApplication.run(ThingsboardServerApplication.class, updateArguments(args));
+    }
+
+    private static String[] updateArguments(String[] args) {
+        if (Arrays.stream(args).noneMatch(arg -> arg.startsWith(SPRING_CONFIG_NAME_KEY))) {
+            String[] modifiedArgs = new String[args.length + 1];
+            System.arraycopy(args, 0, modifiedArgs, 0, args.length);
+            modifiedArgs[args.length] = DEFAULT_SPRING_CONFIG_PARAM;
+            return modifiedArgs;
+        }
+        return args;
+    }
+}
diff --git a/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
new file mode 100644
index 0000000..3af69b4
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/utils/MiscUtils.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.utils;
+
+import com.google.common.hash.HashFunction;
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.Charset;
+
+
+/**
+ * @author Andrew Shvayka
+ */
+public class MiscUtils {
+
+    public static final Charset UTF8 = Charset.forName("UTF-8");
+
+    public static String missingProperty(String propertyName) {
+        return "The " + propertyName + " property need to be set!";
+    }
+
+    public static HashFunction forName(String name) {
+        switch (name) {
+            case "murmur3_32":
+                return Hashing.murmur3_32();
+            case "murmur3_128":
+                return Hashing.murmur3_128();
+            case "crc32":
+                return Hashing.crc32();
+            case "md5":
+                return Hashing.md5();
+            default:
+                throw new IllegalArgumentException("Can't find hash function with name " + name);
+        }
+    }
+}
diff --git a/application/src/main/proto/cluster.proto b/application/src/main/proto/cluster.proto
new file mode 100644
index 0000000..4ac58cf
--- /dev/null
+++ b/application/src/main/proto/cluster.proto
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+package cluster;
+
+option java_package = "org.thingsboard.server.gen.cluster";
+option java_outer_classname = "ClusterAPIProtos";
+
+message ServerAddress {
+  string host = 1;
+  int32 port = 2;
+}
+
+message Uid {
+  sint64 pluginUuidMsb = 1;
+  sint64 pluginUuidLsb = 2;
+}
+
+message PluginAddress {
+  Uid pluginId = 1;
+  Uid tenantId = 2;
+}
+
+message ToPluginRpcMessage {
+  PluginAddress address = 1;
+  int32 clazz = 2;
+  bytes data = 3;
+}
+
+message ToDeviceActorRpcMessage {
+  bytes data = 1;
+}
+
+message ToDeviceSessionActorRpcMessage {
+  bytes data = 1;
+}
+
+message ToDeviceActorNotificationRpcMessage {
+  bytes data = 1;
+}
+
+message ToAllNodesRpcMessage {
+  bytes data = 1;
+}
+
+message ConnectRpcMessage {
+  ServerAddress serverAddress = 1;
+}
+
+message ToDeviceRpcRequestRpcMessage {
+  PluginAddress address = 1;
+  Uid deviceTenantId = 2;
+  Uid deviceId = 3;
+
+  Uid msgId = 4;
+  bool oneway = 5;
+  int64 expTime = 6;
+  string method = 7;
+  string params = 8;
+}
+
+message ToPluginRpcResponseRpcMessage {
+  PluginAddress address = 1;
+
+  Uid msgId = 2;
+  string response = 3;
+  string error = 4;
+}
+
+message ToRpcServerMessage {
+  ConnectRpcMessage connectMsg = 1;
+  ToPluginRpcMessage toPluginRpcMsg = 2;
+  ToDeviceActorRpcMessage toDeviceActorRpcMsg = 3;
+  ToDeviceSessionActorRpcMessage toDeviceSessionActorRpcMsg = 4;
+  ToDeviceActorNotificationRpcMessage toDeviceActorNotificationRpcMsg = 5;
+  ToAllNodesRpcMessage toAllNodesRpcMsg = 6;
+  ToDeviceRpcRequestRpcMessage toDeviceRpcRequestRpcMsg = 7;
+  ToPluginRpcResponseRpcMessage toPluginRpcResponseRpcMsg = 8;
+}
+
+service ClusterRpcService {
+  rpc handlePluginMsgs(stream ToRpcServerMessage) returns (stream ToRpcServerMessage) {}
+}
+
diff --git a/application/src/main/proto/discovery.proto b/application/src/main/proto/discovery.proto
new file mode 100644
index 0000000..17db5c1
--- /dev/null
+++ b/application/src/main/proto/discovery.proto
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+package discovery;
+
+option java_package = "org.thingsboard.server.gen.discovery";
+option java_outer_classname = "ServerInstanceProtos";
+
+message ServerInfo {
+  string host = 1;
+  int32 port = 2;
+  int64 ts = 3;
+}
diff --git a/application/src/main/resources/actor-system.conf b/application/src/main/resources/actor-system.conf
new file mode 100644
index 0000000..7bd80ab
--- /dev/null
+++ b/application/src/main/resources/actor-system.conf
@@ -0,0 +1,163 @@
+#
+# Copyright © 2016 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+akka {
+  # JVM shutdown, System.exit(-1), in case of a fatal error,
+  # such as OutOfMemoryError
+  jvm-exit-on-fatal-error = off
+  loglevel = "INFO"
+  loggers = ["akka.event.slf4j.Slf4jLogger"]
+}
+
+# This dispatcher is used for app
+app-dispatcher {
+  type = Dispatcher
+  executor = "fork-join-executor"
+  fork-join-executor {
+      # Min number of threads to cap factor-based parallelism number to
+      parallelism-min = 2
+      # Max number of threads to cap factor-based parallelism number to
+      parallelism-max = 12
+      
+      # The parallelism factor is used to determine thread pool size using the
+      # following formula: ceil(available processors * factor). Resulting size
+      # is then bounded by the parallelism-min and parallelism-max values.
+      parallelism-factor = 1.0
+  }
+  # How long time the dispatcher will wait for new actors until it shuts down
+  shutdown-timeout = 1s
+  
+  # Throughput defines the number of messages that are processed in a batch
+  # before the thread is returned to the pool. Set to 1 for as fair as possible.
+  throughput = 5
+}
+
+# This dispatcher is used for rpc actors
+rpc-dispatcher {
+  type = Dispatcher
+  executor = "fork-join-executor"
+  fork-join-executor {
+      # Min number of threads to cap factor-based parallelism number to
+      parallelism-min = 2
+      # Max number of threads to cap factor-based parallelism number to
+      parallelism-max = 12
+
+      # The parallelism factor is used to determine thread pool size using the
+      # following formula: ceil(available processors * factor). Resulting size
+      # is then bounded by the parallelism-min and parallelism-max values.
+      parallelism-factor = 0.5
+  }
+  # How long time the dispatcher will wait for new actors until it shuts down
+  shutdown-timeout = 1s
+
+  # Throughput defines the number of messages that are processed in a batch
+  # before the thread is returned to the pool. Set to 1 for as fair as possible.
+  throughput = 5
+}
+
+# This dispatcher is used for auth
+core-dispatcher {
+  type = Dispatcher
+  executor = "fork-join-executor"
+  fork-join-executor {
+      # Min number of threads to cap factor-based parallelism number to
+      parallelism-min = 2
+      # Max number of threads to cap factor-based parallelism number to
+      parallelism-max = 12
+
+      # The parallelism factor is used to determine thread pool size using the
+      # following formula: ceil(available processors * factor). Resulting size
+      # is then bounded by the parallelism-min and parallelism-max values.
+      parallelism-factor = 1.0
+  }
+  # How long time the dispatcher will wait for new actors until it shuts down
+  shutdown-timeout = 1s
+
+  # Throughput defines the number of messages that are processed in a batch
+  # before the thread is returned to the pool. Set to 1 for as fair as possible.
+  throughput = 5
+}
+
+# This dispatcher is used for rule actors
+rule-dispatcher {
+  type = Dispatcher
+  executor = "fork-join-executor"
+  fork-join-executor {
+      # Min number of threads to cap factor-based parallelism number to
+      parallelism-min = 2
+      # Max number of threads to cap factor-based parallelism number to
+      parallelism-max = 12
+
+      # The parallelism factor is used to determine thread pool size using the
+      # following formula: ceil(available processors * factor). Resulting size
+      # is then bounded by the parallelism-min and parallelism-max values.
+      parallelism-factor = 1.0
+  }
+  # How long time the dispatcher will wait for new actors until it shuts down
+  shutdown-timeout = 1s
+
+  # Throughput defines the number of messages that are processed in a batch
+  # before the thread is returned to the pool. Set to 1 for as fair as possible.
+  throughput = 5
+}
+
+# This dispatcher is used for rule actors
+plugin-dispatcher {
+  type = Dispatcher
+  executor = "fork-join-executor"
+  fork-join-executor {
+      # Min number of threads to cap factor-based parallelism number to
+      parallelism-min = 2
+      # Max number of threads to cap factor-based parallelism number to
+      parallelism-max = 12
+
+      # The parallelism factor is used to determine thread pool size using the
+      # following formula: ceil(available processors * factor). Resulting size
+      # is then bounded by the parallelism-min and parallelism-max values.
+      parallelism-factor = 1.0
+  }
+  # How long time the dispatcher will wait for new actors until it shuts down
+  shutdown-timeout = 1s
+
+  # Throughput defines the number of messages that are processed in a batch
+  # before the thread is returned to the pool. Set to 1 for as fair as possible.
+  throughput = 5
+}
+
+
+# This dispatcher is used for rule actors
+session-dispatcher {
+  type = Dispatcher
+  executor = "fork-join-executor"
+  fork-join-executor {
+      # Min number of threads to cap factor-based parallelism number to
+      parallelism-min = 2
+      # Max number of threads to cap factor-based parallelism number to
+      parallelism-max = 12
+
+      # The parallelism factor is used to determine thread pool size using the
+      # following formula: ceil(available processors * factor). Resulting size
+      # is then bounded by the parallelism-min and parallelism-max values.
+      parallelism-factor = 1.0
+  }
+  # How long time the dispatcher will wait for new actors until it shuts down
+  shutdown-timeout = 1s
+
+  # Throughput defines the number of messages that are processed in a batch
+  # before the thread is returned to the pool. Set to 1 for as fair as possible.
+  throughput = 5
+}
\ No newline at end of file
diff --git a/application/src/main/resources/banner.txt b/application/src/main/resources/banner.txt
new file mode 100644
index 0000000..791f878
--- /dev/null
+++ b/application/src/main/resources/banner.txt
@@ -0,0 +1,3 @@
+ ===================================================
+ :: ${application.title} ::      ${application.formatted-version}
+ ===================================================
diff --git a/application/src/main/resources/i18n/messages.properties b/application/src/main/resources/i18n/messages.properties
new file mode 100644
index 0000000..a78fbe0
--- /dev/null
+++ b/application/src/main/resources/i18n/messages.properties
@@ -0,0 +1,5 @@
+test.message.subject=Test message from Thingsboard
+activation.subject=Your account activation on Thingsboard
+account.activated.subject=Thingsboard - your account has been activated
+reset.password.subject=Thingsboard - Password reset has been requested
+password.was.reset.subject=Thingsboard - your account password has been reset
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
new file mode 100644
index 0000000..5506578
--- /dev/null
+++ b/application/src/main/resources/logback.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE configuration>
+<configuration>
+
+    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.thingsboard.server" level="TRACE" />
+    <logger name="akka" level="INFO" />
+
+    <root level="INFO">
+        <appender-ref ref="STDOUT"/>
+    </root>
+
+</configuration>
\ No newline at end of file
diff --git a/application/src/main/resources/templates/account.activated.vm b/application/src/main/resources/templates/account.activated.vm
new file mode 100644
index 0000000..4913acc
--- /dev/null
+++ b/application/src/main/resources/templates/account.activated.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Account Activated</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+  body {
+    padding: 0 !important;
+  }
+  h1 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h2 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h3 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h4 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h1 {
+    font-size: 22px !important;
+  }
+  h2 {
+    font-size: 18px !important;
+  }
+  h3 {
+    font-size: 16px !important;
+  }
+  .container {
+    padding: 0 !important; width: 100% !important;
+  }
+  .content {
+    padding: 0 !important;
+  }
+  .content-wrap {
+    padding: 10px !important;
+  }
+  .invoice {
+    width: 100% !important;
+  }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+		<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+			<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+				<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+							<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">								
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<h2>Your Thingsboard account has been activated</h2>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										Congratulations! Your Thingsboard account has been activated.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										Now you can login to your Thingsboard space.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<a href="$loginLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Login</a>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										&mdash; The Thingsboard
+									</td>
+								</tr></table></td>
+							</tr>
+				</table>
+				<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+					<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+						<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+							<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+		</td>
+		<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+	</tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/activation.vm b/application/src/main/resources/templates/activation.vm
new file mode 100644
index 0000000..7d2beb3
--- /dev/null
+++ b/application/src/main/resources/templates/activation.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Account Activation</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+  body {
+    padding: 0 !important;
+  }
+  h1 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h2 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h3 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h4 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h1 {
+    font-size: 22px !important;
+  }
+  h2 {
+    font-size: 18px !important;
+  }
+  h3 {
+    font-size: 16px !important;
+  }
+  .container {
+    padding: 0 !important; width: 100% !important;
+  }
+  .content {
+    padding: 0 !important;
+  }
+  .content-wrap {
+    padding: 10px !important;
+  }
+  .invoice {
+    width: 100% !important;
+  }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+		<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+			<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+				<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+							<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">								
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<h2>Activate your Thingsboard account</h2>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										To confirm your email address and choose a password, just click the button below.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										We may need to send you critical information about our service and it is important that we have an accurate email address.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<a href="$activationLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Activate your account</a>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										&mdash; The Thingsboard
+									</td>
+								</tr></table></td>
+							</tr>
+				</table>
+				<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+					<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+						<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+							<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+		</td>
+		<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+	</tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/password.was.reset.vm b/application/src/main/resources/templates/password.was.reset.vm
new file mode 100644
index 0000000..22beff6
--- /dev/null
+++ b/application/src/main/resources/templates/password.was.reset.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Account Password Has Been Reset</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+  body {
+    padding: 0 !important;
+  }
+  h1 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h2 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h3 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h4 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h1 {
+    font-size: 22px !important;
+  }
+  h2 {
+    font-size: 18px !important;
+  }
+  h3 {
+    font-size: 16px !important;
+  }
+  .container {
+    padding: 0 !important; width: 100% !important;
+  }
+  .content {
+    padding: 0 !important;
+  }
+  .content-wrap {
+    padding: 10px !important;
+  }
+  .invoice {
+    width: 100% !important;
+  }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+		<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+			<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+				<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+							<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">								
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<h2>Your Thingsboard account password has been reset</h2>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										You have successfully created new password for your Thingsboard account.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										Now you can login to your Thingsboard space using your newly created password.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<a href="$loginLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Login</a>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										&mdash; The Thingsboard
+									</td>
+								</tr></table></td>
+							</tr>
+				</table>
+				<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+					<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+						<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+							<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+		</td>
+		<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+	</tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/reset.password.vm b/application/src/main/resources/templates/reset.password.vm
new file mode 100644
index 0000000..18ebcc0
--- /dev/null
+++ b/application/src/main/resources/templates/reset.password.vm
@@ -0,0 +1,122 @@
+#*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Reset Password Request</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+  body {
+    padding: 0 !important;
+  }
+  h1 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h2 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h3 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h4 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h1 {
+    font-size: 22px !important;
+  }
+  h2 {
+    font-size: 18px !important;
+  }
+  h3 {
+    font-size: 16px !important;
+  }
+  .container {
+    padding: 0 !important; width: 100% !important;
+  }
+  .content {
+    padding: 0 !important;
+  }
+  .content-wrap {
+    padding: 10px !important;
+  }
+  .invoice {
+    width: 100% !important;
+  }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+		<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+			<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+				<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+							<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">								
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<h2>Password reset has been requested</h2>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										You have requested password reset for your Thingsboard account.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										Click below in order to proceed password reset procedure.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" itemprop="handler" itemscope itemtype="http://schema.org/HttpActionHandler" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<a href="$passwordResetLink" class="btn-primary" itemprop="url" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">Reset password</a>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										&mdash; The Thingsboard
+									</td>
+								</tr></table></td>
+							</tr>
+				</table>
+				<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+					<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+						<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+							<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+		</td>
+		<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+	</tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/templates/test.vm b/application/src/main/resources/templates/test.vm
new file mode 100644
index 0000000..ecd3d15
--- /dev/null
+++ b/application/src/main/resources/templates/test.vm
@@ -0,0 +1,112 @@
+#*
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *#
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+<head>
+<meta name="viewport" content="width=device-width" />
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>Thingsboard - Test Message</title>
+
+
+<style type="text/css">
+img {
+max-width: 100%;
+}
+body {
+-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
+}
+body {
+background-color: #f6f6f6;
+}
+@media only screen and (max-width: 640px) {
+  body {
+    padding: 0 !important;
+  }
+  h1 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h2 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h3 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h4 {
+    font-weight: 800 !important; margin: 20px 0 5px !important;
+  }
+  h1 {
+    font-size: 22px !important;
+  }
+  h2 {
+    font-size: 18px !important;
+  }
+  h3 {
+    font-size: 16px !important;
+  }
+  .container {
+    padding: 0 !important; width: 100% !important;
+  }
+  .content {
+    padding: 0 !important;
+  }
+  .content-wrap {
+    padding: 10px !important;
+  }
+  .invoice {
+    width: 100% !important;
+  }
+}
+</style>
+</head>
+
+<body itemscope itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
+
+<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+		<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
+			<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
+				<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
+							<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">								
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										<h2>Test message from Thingsboard</h2>
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										This email is indicating that your outgoing mail settings were set up correctly.
+									</td>
+								</tr>
+								<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+									<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
+										&mdash; The Thingsboard
+									</td>
+								</tr></table></td>
+							</tr>
+				</table>
+				<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
+					<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+						<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
+							<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:$targetEmail" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">$targetEmail</a> by Thingsboard.</td>
+						</tr>
+					</table>
+				</div>
+			</div>
+		</td>
+		<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
+	</tr>
+</table>
+</body>
+</html>
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
new file mode 100644
index 0000000..7a8f58f
--- /dev/null
+++ b/application/src/main/resources/thingsboard.yml
@@ -0,0 +1,176 @@
+#
+# Copyright © 2016 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+server:
+  # Server bind address
+  address: "${HTTP_BIND_ADDRESS:0.0.0.0}"
+  # Server bind port
+  port: "${HTTP_BIND_PORT:8080}"
+# Uncomment the following section to enable ssl
+#  ssl:
+#    key-store: classpath:keystore/keystore.p12
+#    key-store-password: thingsboard
+#    keyStoreType: PKCS12
+#    keyAlias: tomcat
+
+# Zookeeper connection parameters. Used for service discovery.
+zk:
+  # Enable/disable zookeeper discovery service.
+  enabled: "${ZOOKEEPER_ENABLED:false}"
+  # Zookeeper connect string
+  url: "${ZOOKEEPER_URL:localhost:2181}"
+  # Zookeeper retry interval in milliseconds
+  retry_interval_ms: "${ZOOKEEPER_RETRY_INTERVAL_MS:3000}"
+  # Zookeeper connection timeout in milliseconds
+  connection_timeout_ms: "${ZOOKEEPER_CONNECTION_TIMEOUT_MS:3000}"
+  # Zookeeper session timeout in milliseconds
+  session_timeout_ms: "${ZOOKEEPER_SESSION_TIMEOUT_MS:3000}"
+  # Name of the directory in zookeeper 'filesystem'
+  zk_dir: "${ZOOKEEPER_NODES_DIR:/thingsboard}"
+
+# RPC connection parameters. Used only in cluster mode only.
+rpc:
+  bind_host: "${RPC_HOST:localhost}"
+  bind_port: "${RPC_PORT:9001}"
+
+# Clustering properties related to consistent-hashing. See architecture docs for more details.
+cluster:
+  # Name of hash function used for consistent hash ring.
+  hash_function_name: "${CLUSTER_HASH_FUNCTION_NAME:murmur3_128}"
+  # Amount of virtual nodes in consistent hash ring.
+  vitrual_nodes_size: "${CLUSTER_VIRTUAL_NODES_SIZE:16}"
+
+# Plugins configuration parameters
+plugins:
+  # Comma seperated package list used during classpath scanning for plugins
+  scan_packages: "${PLUGINS_SCAN_PACKAGES:org.thingsboard.server.extensions}"
+
+# JWT Token parameters
+security.jwt:
+  tokenExpirationTime: "${JWT_TOKEN_EXPIRATION_TIME:900}" # Number of seconds (15 mins)
+  refreshTokenExpTime: "${JWT_REFRESH_TOKEN_EXPIRATION_TIME:3600}" # Seconds (1 hour)
+  tokenIssuer: "${JWT_TOKEN_ISSUER:thingsboard.io}"
+  tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}"
+
+# Device communication protocol parameters
+http:
+  request_timeout: "${HTTP_REQUEST_TIMEOUT:60000}"
+
+# MQTT server parameters
+mqtt:
+  bind_address: "${MQTT_BIND_ADDRESS:0.0.0.0}"
+  bind_port: "${MQTT_BIND_PORT:1883}"
+  adaptor: "${MQTT_ADAPTOR_NAME:JsonMqttAdaptor}"
+  timeout: "${MQTT_TIMEOUT:10000}"
+# Uncomment the following lines to enable ssl for MQTT
+#  ssl:
+#    key-store: keystore/mqttserver.jks
+#    key-store-password: password
+#    keyStoreType: JKS
+# TrustStore can be the same as KeyStore
+#    trust-store: keystore/mqttserver.jks
+#    trust-store-password: password
+#    trustStoreType: JKS
+
+# CoAP server parameters
+coap:
+  bind_address: "${COAP_BIND_ADDRESS:0.0.0.0}"
+  bind_port: "${COAP_BIND_PORT:5683}"
+  adaptor:  "${COAP_ADAPTOR_NAME:JsonCoapAdaptor}"
+  timeout: "${COAP_TIMEOUT:10000}"
+
+# Cassandra driver configuration parameters
+cassandra:
+  # Thingsboard cluster name
+  cluster_name: "${CASSANDRA_CLUSTER_NAME:Thingsboard Cluster}"
+  # Thingsboard keyspace name
+  keyspace_name: "${CASSANDRA_KEYSPACE_NAME:thingsboard}"
+  # Specify node list
+  url: "${CASSANDRA_URL:127.0.0.1:9042}"
+  # Enable/disable secure connection
+  ssl: "${CASSANDRA_USE_SSL:false}"
+  # Enable/disable JMX
+  jmx: "${CASSANDRA_USE_JMX:true}"
+  # Enable/disable metrics collection.
+  metrics: "${CASSANDRA_DISABLE_METRICS:true}"
+  # NONE SNAPPY LZ4
+  compression: "${CASSANDRA_COMPRESSION:none}"
+  # Specify cassandra claster initialization timeout (if no hosts available during startup)
+  init_timeout_ms: "${CASSANDRA_CLUSTER_INIT_TIMEOUT_MS:300000}"
+  # Specify cassandra claster initialization retry interval (if no hosts available during startup)
+  init_retry_interval_ms: "${CASSANDRA_CLUSTER_INIT_RETRY_INTERVAL_MS:3000}"
+
+  # Credential parameters #
+  credentials: "${CASSANDRA_USE_CREDENTIALS:false}"
+  # Specify your username
+  username: "${CASSANDRA_USERNAME:}"
+  # Specify your password
+  password: "${CASSANDRA_PASSWORD:}"
+
+  # Cassandra cluster connection socket parameters #
+  socket:
+    connect_timeout: "${CASSANDRA_SOCKET_TIMEOUT:5000}"
+    read_timeout: "${CASSANDRA_SOCKET_READ_TIMEOUT:20000}"
+    keep_alive: "${CASSANDRA_SOCKET_KEEP_ALIVE:true}"
+    reuse_address: "${CASSANDRA_SOCKET_REUSE_ADDRESS:true}"
+    so_linger: "${CASSANDRA_SOCKET_SO_LINGER:}"
+    tcp_no_delay: "${CASSANDRA_SOCKET_TCP_NO_DELAY:false}"
+    receive_buffer_size: "${CASSANDRA_SOCKET_RECEIVE_BUFFER_SIZE:}"
+    send_buffer_size: "${CASSANDRA_SOCKET_SEND_BUFFER_SIZE:}"
+
+  # Cassandra cluster connection query parameters  #
+  query:
+    read_consistency_level: "${CASSANDRA_READ_CONSISTENCY_LEVEL:ONE}"
+    write_consistency_level: "${CASSANDRA_WRITE_CONSISTENCY_LEVEL:ONE}"
+    default_fetch_size: "${CASSANDRA_DEFAULT_FETCH_SIZE:2000}"
+    # Specify partitioning size for timestamp key-value storage. Example MINUTES, HOURS, DAYS, MONTHS
+    ts_key_value_partitioning: "${TS_KV_PARTITIONING:MONTHS}"
+    # Specify max partitions per request
+    max_limit_per_request: "${TS_KV_MAX_LIMIT_PER_REQUEST:1000}"
+
+# Actor system parameters
+actors:
+  session:
+    sync:
+      # Default timeout for processing request using synchronous session (HTTP, CoAP) in milliseconds
+      timeout: "${ACTORS_SESSION_SYNC_TIMEOUT:10000}"
+  plugin:
+    # Default timeout for termination of the plugin actor after it is stopped
+    termination.delay: "${ACTORS_PLUGIN_TERMINATION_DELAY:60000}"
+    # Default timeout for processing of particular message by particular plugin
+    processing.timeout: "${ACTORS_PLUGIN_TIMEOUT:60000}"
+    # Errors for particular actor are persisted once per specified amount of milliseconds
+    error_persist_frequency: "${ACTORS_PLUGIN_ERROR_FREQUENCY:3000}"
+  rule:
+    # Default timeout for termination of the rule actor after it is stopped
+    termination.delay: "${ACTORS_RULE_TERMINATION_DELAY:30000}"
+    # Errors for particular actor are persisted once per specified amount of milliseconds
+    error_persist_frequency: "${ACTORS_RULE_ERROR_FREQUENCY:3000}"
+  statistics:
+    # Enable/disable actor statistics
+    enabled: "${ACTORS_STATISTICS_ENABLED:true}"
+    persist_frequency: "${ACTORS_STATISTICS_PERSIST_FREQUENCY:60000}"
+
+# Cache parameters
+cache:
+  # Enable/disable cache functionality.
+  enabled: "${CACHE_ENABLED:true}"
+  device_credentials:
+    # default time to store device credentials in cache, in seconds
+    time_to_live: "${DEVICE_CREDENTIAL_CACHE_TTL:3600}"
+    # default maximum size of device credentials cache
+    max_size: "${DEVICE_CREDENTIAL_CACHE_MAX_SIZE:1000000}"
+
diff --git a/application/src/main/scripts/control/deb/postinst b/application/src/main/scripts/control/deb/postinst
new file mode 100644
index 0000000..d4066c0
--- /dev/null
+++ b/application/src/main/scripts/control/deb/postinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+chown -R ${pkg.name}: ${pkg.logFolder}
+chown -R ${pkg.name}: ${pkg.installFolder}
+update-rc.d ${pkg.name} defaults
+
diff --git a/application/src/main/scripts/control/deb/postrm b/application/src/main/scripts/control/deb/postrm
new file mode 100644
index 0000000..6186580
--- /dev/null
+++ b/application/src/main/scripts/control/deb/postrm
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+update-rc.d -f ${pkg.name} remove
diff --git a/application/src/main/scripts/control/deb/preinst b/application/src/main/scripts/control/deb/preinst
new file mode 100644
index 0000000..6be5959
--- /dev/null
+++ b/application/src/main/scripts/control/deb/preinst
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+if ! getent group ${pkg.name} >/dev/null; then
+    addgroup --system ${pkg.name}
+fi
+
+if ! getent passwd ${pkg.name} >/dev/null; then
+    adduser --quiet \
+            --system \
+            --ingroup ${pkg.name} \
+            --quiet \
+            --disabled-login \
+            --disabled-password \
+            --home ${pkg.installFolder} \
+            --no-create-home \
+            -gecos "Thingsboard application" \
+            ${pkg.name}
+fi
diff --git a/application/src/main/scripts/control/deb/prerm b/application/src/main/scripts/control/deb/prerm
new file mode 100644
index 0000000..898d3ef
--- /dev/null
+++ b/application/src/main/scripts/control/deb/prerm
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+if [ -e /var/run/${pkg.name}/${pkg.name}.pid ]; then
+    service ${pkg.name} stop
+fi
diff --git a/application/src/main/scripts/control/rpm/postinst b/application/src/main/scripts/control/rpm/postinst
new file mode 100644
index 0000000..8a7a88f
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/postinst
@@ -0,0 +1,9 @@
+#!/bin/sh
+
+chown -R ${pkg.name}: ${pkg.logFolder}
+chown -R ${pkg.name}: ${pkg.installFolder}
+
+if [ $1 -eq 1 ] ; then
+        # Initial installation
+        systemctl --no-reload enable ${pkg.name}.service >/dev/null 2>&1 || :
+fi
diff --git a/application/src/main/scripts/control/rpm/postrm b/application/src/main/scripts/control/rpm/postrm
new file mode 100644
index 0000000..8e1f8a2
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/postrm
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ $1 -ge 1 ] ; then
+        # Package upgrade, not uninstall
+        systemctl try-restart ${pkg.name}.service >/dev/null 2>&1 || :
+fi
diff --git a/application/src/main/scripts/control/rpm/preinst b/application/src/main/scripts/control/rpm/preinst
new file mode 100644
index 0000000..e19fc88
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/preinst
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+getent group ${pkg.name} >/dev/null || groupadd -r ${pkg.name}
+getent passwd ${pkg.name} >/dev/null || \
+useradd -d ${pkg.installFolder} -g ${pkg.name} -M -r ${pkg.name} -s /sbin/nologin \
+-c "Thingsboard application"
diff --git a/application/src/main/scripts/control/rpm/prerm b/application/src/main/scripts/control/rpm/prerm
new file mode 100644
index 0000000..accb487
--- /dev/null
+++ b/application/src/main/scripts/control/rpm/prerm
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+if [ $1 -eq 0 ] ; then
+        # Package removal, not upgrade
+        systemctl --no-reload disable --now ${pkg.name}.service > /dev/null 2>&1 || :
+fi
diff --git a/application/src/main/scripts/control/thingsboard.service b/application/src/main/scripts/control/thingsboard.service
new file mode 100644
index 0000000..d456fc0
--- /dev/null
+++ b/application/src/main/scripts/control/thingsboard.service
@@ -0,0 +1,11 @@
+[Unit]
+Description=${pkg.name}
+After=syslog.target
+
+[Service]
+User=${pkg.name}
+ExecStart=${pkg.installFolder}/bin/${pkg.name}.jar
+SuccessExitStatus=143
+
+[Install]
+WantedBy=multi-user.target
diff --git a/application/src/test/java/org/thingsboard/server/actors/ActorsTestSuite.java b/application/src/test/java/org/thingsboard/server/actors/ActorsTestSuite.java
new file mode 100644
index 0000000..481868b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/ActorsTestSuite.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors;
+
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.actors.*Test"})
+public class ActorsTestSuite {
+}
diff --git a/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
new file mode 100644
index 0000000..10471d6
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/DefaultActorServiceTest.java
@@ -0,0 +1,242 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.*;
+
+import org.thingsboard.server.actors.service.DefaultActorService;
+import org.thingsboard.server.common.data.id.*;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.dao.attributes.AttributesService;
+import org.thingsboard.server.dao.event.EventService;
+import org.thingsboard.server.gen.discovery.ServerInstanceProtos;
+import org.thingsboard.server.service.cluster.discovery.DiscoveryService;
+import org.thingsboard.server.service.cluster.discovery.ServerInstance;
+import org.thingsboard.server.service.cluster.routing.ClusterRoutingService;
+import org.thingsboard.server.service.cluster.rpc.ClusterRpcService;
+import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.core.BasicTelemetryUploadRequest;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.thingsboard.server.dao.plugin.PluginService;
+import org.thingsboard.server.dao.rule.RuleService;
+import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class DefaultActorServiceTest {
+
+    private static final TenantId SYSTEM_TENANT = new TenantId(ModelConstants.NULL_UUID);
+
+    private static final String PLUGIN_ID = "9fb2e951-e298-4acb-913a-db69af8a15f4";
+    private static final String FILTERS_CONFIGURATION =
+            "[{\"clazz\":\"org.thingsboard.server.extensions.core.filter.MsgTypeFilter\", \"name\":\"TelemetryFilter\", \"configuration\": {\"messageTypes\":[\"POST_TELEMETRY\",\"POST_ATTRIBUTES\",\"GET_ATTRIBUTES\"]}}]";
+    private static final String ACTION_CONFIGURATION = "{\"pluginToken\":\"telemetry\", \"clazz\":\"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction\", \"name\":\"TelemetryMsgConverterAction\", \"configuration\":{}}";
+    private static final String PLUGIN_CONFIGURATION = "{}";
+    private DefaultActorService actorService;
+    private ActorSystemContext actorContext;
+
+    private PluginService pluginService;
+    private RuleService ruleService;
+    private DeviceAuthService deviceAuthService;
+    private DeviceService deviceService;
+    private TimeseriesService tsService;
+    private TenantService tenantService;
+    private ClusterRpcService rpcService;
+    private DiscoveryService discoveryService;
+    private ClusterRoutingService routingService;
+    private AttributesService attributesService;
+    private ComponentDiscoveryService componentService;
+    private EventService eventService;
+    private ServerInstance serverInstance;
+
+    private RuleMetaData ruleMock;
+    private PluginMetaData pluginMock;
+    private RuleId ruleId = new RuleId(UUID.randomUUID());
+    private PluginId pluginId = new PluginId(UUID.fromString(PLUGIN_ID));
+    private TenantId tenantId = new TenantId(UUID.randomUUID());
+
+
+    @Before
+    public void before() throws Exception {
+        actorService = new DefaultActorService();
+        actorContext = new ActorSystemContext();
+
+        tenantService = mock(TenantService.class);
+        pluginService = mock(PluginService.class);
+        ruleService = mock(RuleService.class);
+        deviceAuthService = mock(DeviceAuthService.class);
+        deviceService = mock(DeviceService.class);
+        tsService = mock(TimeseriesService.class);
+        rpcService = mock(ClusterRpcService.class);
+        discoveryService = mock(DiscoveryService.class);
+        routingService = mock(ClusterRoutingService.class);
+        attributesService = mock(AttributesService.class);
+        componentService = mock(ComponentDiscoveryService.class);
+        eventService = mock(EventService.class);
+        serverInstance = new ServerInstance(ServerInstanceProtos.ServerInfo.newBuilder().setHost("localhost").setPort(8080).build());
+
+        ReflectionTestUtils.setField(actorService, "actorContext", actorContext);
+        ReflectionTestUtils.setField(actorService, "rpcService", rpcService);
+        ReflectionTestUtils.setField(actorService, "discoveryService", discoveryService);
+
+        ReflectionTestUtils.setField(actorContext, "syncSessionTimeout", 10000L);
+        ReflectionTestUtils.setField(actorContext, "pluginActorTerminationDelay", 10000L);
+        ReflectionTestUtils.setField(actorContext, "pluginErrorPersistFrequency", 10000L);
+        ReflectionTestUtils.setField(actorContext, "ruleActorTerminationDelay", 10000L);
+        ReflectionTestUtils.setField(actorContext, "ruleErrorPersistFrequency", 10000L);
+        ReflectionTestUtils.setField(actorContext, "pluginProcessingTimeout", 60000L);
+        ReflectionTestUtils.setField(actorContext, "tenantService", tenantService);
+        ReflectionTestUtils.setField(actorContext, "pluginService", pluginService);
+        ReflectionTestUtils.setField(actorContext, "ruleService", ruleService);
+        ReflectionTestUtils.setField(actorContext, "deviceAuthService", deviceAuthService);
+        ReflectionTestUtils.setField(actorContext, "deviceService", deviceService);
+        ReflectionTestUtils.setField(actorContext, "tsService", tsService);
+        ReflectionTestUtils.setField(actorContext, "rpcService", rpcService);
+        ReflectionTestUtils.setField(actorContext, "discoveryService", discoveryService);
+        ReflectionTestUtils.setField(actorContext, "tsService", tsService);
+        ReflectionTestUtils.setField(actorContext, "routingService", routingService);
+        ReflectionTestUtils.setField(actorContext, "attributesService", attributesService);
+        ReflectionTestUtils.setField(actorContext, "componentService", componentService);
+        ReflectionTestUtils.setField(actorContext, "eventService", eventService);
+
+
+        when(routingService.resolve(any())).thenReturn(Optional.empty());
+
+        when(discoveryService.getCurrentServer()).thenReturn(serverInstance);
+
+        ruleMock = mock(RuleMetaData.class);
+        when(ruleMock.getId()).thenReturn(ruleId);
+        when(ruleMock.getState()).thenReturn(ComponentLifecycleState.ACTIVE);
+        when(ruleMock.getPluginToken()).thenReturn("telemetry");
+        TextPageData<RuleMetaData> systemRules = new TextPageData<>(Collections.emptyList(), null, false);
+        TextPageData<RuleMetaData> tenantRules = new TextPageData<>(Collections.singletonList(ruleMock), null, false);
+        when(ruleService.findSystemRules(any())).thenReturn(systemRules);
+        when(ruleService.findTenantRules(any(), any())).thenReturn(tenantRules);
+        when(ruleService.findRuleById(ruleId)).thenReturn(ruleMock);
+
+        pluginMock = mock(PluginMetaData.class);
+        when(pluginMock.getTenantId()).thenReturn(SYSTEM_TENANT);
+        when(pluginMock.getId()).thenReturn(pluginId);
+        when(pluginMock.getState()).thenReturn(ComponentLifecycleState.ACTIVE);
+        TextPageData<PluginMetaData> systemPlugins = new TextPageData<>(Collections.singletonList(pluginMock), null, false);
+        TextPageData<PluginMetaData> tenantPlugins = new TextPageData<>(Collections.emptyList(), null, false);
+        when(pluginService.findSystemPlugins(any())).thenReturn(systemPlugins);
+        when(pluginService.findTenantPlugins(any(), any())).thenReturn(tenantPlugins);
+        when(pluginService.findPluginByApiToken("telemetry")).thenReturn(pluginMock);
+        when(pluginService.findPluginById(pluginId)).thenReturn(pluginMock);
+
+        TextPageData<Tenant> tenants = new TextPageData<>(Collections.emptyList(), null, false);
+        when(tenantService.findTenants(any())).thenReturn(tenants);
+    }
+
+    private void initActorSystem() {
+        actorService.initActorSystem();
+    }
+
+    @After
+    public void after() {
+        actorService.stopActorSystem();
+    }
+
+    @Test
+    public void testBasicPostWithSyncSession() throws Exception {
+        SessionContext ssnCtx = mock(SessionContext.class);
+        KvEntry entry1 = new StringDataEntry("key1", "value1");
+        KvEntry entry2 = new StringDataEntry("key2", "value2");
+        BasicTelemetryUploadRequest telemetry = new BasicTelemetryUploadRequest();
+        long ts = 42;
+        telemetry.add(ts, entry1);
+        telemetry.add(ts, entry2);
+        BasicAdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ssnCtx, telemetry);
+
+        DeviceId deviceId = new DeviceId(UUID.randomUUID());
+
+        DeviceCredentialsFilter filter = new DeviceTokenCredentials("token1");
+        Device device = mock(Device.class);
+
+        when(device.getId()).thenReturn(deviceId);
+        when(device.getTenantId()).thenReturn(tenantId);
+        when(ssnCtx.getSessionId()).thenReturn(new DummySessionID("session1"));
+        when(ssnCtx.getSessionType()).thenReturn(SessionType.SYNC);
+        when(deviceAuthService.process(filter)).thenReturn(DeviceAuthResult.of(deviceId));
+        when(deviceService.findDeviceById(deviceId)).thenReturn(device);
+
+        ObjectMapper ruleMapper = new ObjectMapper();
+        when(ruleMock.getFilters()).thenReturn(ruleMapper.readTree(FILTERS_CONFIGURATION));
+        when(ruleMock.getAction()).thenReturn(ruleMapper.readTree(ACTION_CONFIGURATION));
+
+        ComponentDescriptor filterComp = new ComponentDescriptor();
+        filterComp.setClazz("org.thingsboard.server.extensions.core.filter.MsgTypeFilter");
+        filterComp.setType(ComponentType.FILTER);
+        when(componentService.getComponent("org.thingsboard.server.extensions.core.filter.MsgTypeFilter"))
+                .thenReturn(Optional.of(filterComp));
+
+        ComponentDescriptor actionComp = new ComponentDescriptor();
+        actionComp.setClazz("org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction");
+        actionComp.setType(ComponentType.ACTION);
+        when(componentService.getComponent("org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction"))
+                .thenReturn(Optional.of(actionComp));
+
+        ObjectMapper pluginMapper = new ObjectMapper();
+        JsonNode pluginAdditionalInfo = pluginMapper.readTree(PLUGIN_CONFIGURATION);
+        when(pluginMock.getConfiguration()).thenReturn(pluginAdditionalInfo);
+        when(pluginMock.getClazz()).thenReturn(TelemetryStoragePlugin.class.getName());
+
+        when(attributesService.findAll(deviceId, DataConstants.CLIENT_SCOPE)).thenReturn(Collections.emptyList());
+
+        initActorSystem();
+        Thread.sleep(1000);
+        actorService.process(new BasicToDeviceActorSessionMsg(device, msg));
+
+        // Check that device data was saved to DB;
+        List<TsKvEntry> expected = new ArrayList<>();
+        expected.add(new BasicTsKvEntry(ts, entry1));
+        expected.add(new BasicTsKvEntry(ts, entry2));
+        verify(tsService, Mockito.timeout(5000)).save(DataConstants.DEVICE, deviceId, expected);
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/actors/DummySessionID.java b/application/src/test/java/org/thingsboard/server/actors/DummySessionID.java
new file mode 100644
index 0000000..ccb9138
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/actors/DummySessionID.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.actors;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public class DummySessionID implements SessionId {
+
+    @Override
+    public String toString() {
+        return id;
+    }
+
+    private final String id;
+    
+    public DummySessionID(String id) {
+        this.id = id;
+    }
+    
+    @Override
+    public String toUidStr() {
+        return id;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        DummySessionID other = (DummySessionID) obj;
+        if (id == null) {
+            if (other.id != null)
+                return false;
+        } else if (!id.equals(other.id))
+            return false;
+        return true;
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
new file mode 100644
index 0000000..538e8f9
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java
@@ -0,0 +1,375 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Header;
+import io.jsonwebtoken.Jwt;
+import io.jsonwebtoken.Jwts;
+import org.apache.commons.lang3.StringUtils;
+import org.hamcrest.Matcher;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationContextLoader;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.mock.http.MockHttpInputMessage;
+import org.springframework.mock.http.MockHttpOutputMessage;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.ResultMatcher;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.context.WebApplicationContext;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UUIDBased;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.config.ThingsboardSecurityConfiguration;
+import org.thingsboard.server.exception.ThingsboardException;
+import org.thingsboard.server.service.mail.MailService;
+import org.thingsboard.server.service.mail.TestMailService;
+import org.thingsboard.server.service.security.auth.rest.LoginRequest;
+import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
+
+@ActiveProfiles("test")
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration(classes=AbstractControllerTest.class, loader=SpringApplicationContextLoader.class)
+@TestPropertySource("classpath:cassandra-test.properties")
+@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS)
+@Configuration
+@EnableAutoConfiguration
+@ComponentScan({"org.thingsboard.server"})
+@WebAppConfiguration
+@IntegrationTest("server.port:0")
+public abstract class AbstractControllerTest {
+
+    protected static final String SYS_ADMIN_EMAIL = "sysadmin@thingsboard.org";
+    private static final String SYS_ADMIN_PASSWORD = "sysadmin";
+    
+    protected static final String TENANT_ADMIN_EMAIL = "tenant@thingsboard.org";
+    private static final String TENANT_ADMIN_PASSWORD = "tenant";
+
+    protected static final String CUSTOMER_USER_EMAIL = "customer@thingsboard.org";
+    private static final String CUSTOMER_USER_PASSWORD = "customer";
+    
+    protected MediaType contentType = new MediaType(MediaType.APPLICATION_JSON.getType(),
+            MediaType.APPLICATION_JSON.getSubtype(),
+            Charset.forName("utf8"));
+
+    
+    protected MockMvc mockMvc;
+    
+    protected String token;
+    protected String refreshToken;
+    protected String username;
+
+    private TenantId tenantId;
+    
+    @SuppressWarnings("rawtypes")
+    private HttpMessageConverter mappingJackson2HttpMessageConverter;
+    
+    @Autowired
+    private WebApplicationContext webApplicationContext;
+    
+    @Autowired
+    void setConverters(HttpMessageConverter<?>[] converters) {
+
+        this.mappingJackson2HttpMessageConverter = Arrays.asList(converters).stream().filter(
+                hmc -> hmc instanceof MappingJackson2HttpMessageConverter).findAny().get();
+
+        Assert.assertNotNull("the JSON message converter must not be null",
+                this.mappingJackson2HttpMessageConverter);
+    }
+    
+    @Before
+    public void setup() throws Exception {
+        if (this.mockMvc == null) {
+            this.mockMvc = webAppContextSetup(webApplicationContext)
+                    .apply(springSecurity()).build();
+        }
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("Tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        tenantId = savedTenant.getId();
+
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(tenantId);
+        tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);
+
+        createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
+
+        Customer customer = new Customer();
+        customer.setTitle("Customer");
+        customer.setTenantId(tenantId);
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        User customerUser = new User();
+        customerUser.setAuthority(Authority.CUSTOMER_USER);
+        customerUser.setTenantId(tenantId);
+        customerUser.setCustomerId(savedCustomer.getId());
+        customerUser.setEmail(CUSTOMER_USER_EMAIL);
+
+        createUserAndLogin(customerUser, CUSTOMER_USER_PASSWORD);
+
+        logout();
+    }
+
+    @After
+    public void teardown() throws Exception {
+        loginSysAdmin();
+        doDelete("/api/tenant/"+tenantId.getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    protected void loginSysAdmin() throws Exception {
+        login(SYS_ADMIN_EMAIL, SYS_ADMIN_PASSWORD);
+    }
+    
+    protected void loginTenantAdmin() throws Exception {
+        login(TENANT_ADMIN_EMAIL, TENANT_ADMIN_PASSWORD);
+    }
+
+    protected void loginCustomerUser() throws Exception {
+        login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
+    }
+    
+    protected User createUserAndLogin(User user, String password) throws Exception {
+        User savedUser = doPost("/api/user", user, User.class);
+        logout();
+        doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
+        .andExpect(status().isPermanentRedirect())
+        .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
+        JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", password).andExpect(status().isOk()), JsonNode.class);
+        validateAndSetJwtToken(tokenInfo, user.getEmail());
+        return savedUser;
+    }
+
+    protected void login(String username, String password) throws Exception {
+        this.token = null;
+        this.refreshToken = null;
+        this.username = null;
+        JsonNode tokenInfo = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
+        validateAndSetJwtToken(tokenInfo, username);
+    }
+
+    protected void refreshToken() throws Exception {
+        this.token = null;
+        JsonNode tokenInfo = readResponse(doPost("/api/auth/token", new RefreshTokenRequest(this.refreshToken)).andExpect(status().isOk()), JsonNode.class);
+        validateAndSetJwtToken(tokenInfo, this.username);
+    }
+
+    protected void validateAndSetJwtToken(JsonNode tokenInfo, String username) {
+        Assert.assertNotNull(tokenInfo);
+        Assert.assertTrue(tokenInfo.has("token"));
+        Assert.assertTrue(tokenInfo.has("refreshToken"));
+        String token = tokenInfo.get("token").asText();
+        String refreshToken = tokenInfo.get("refreshToken").asText();
+        validateJwtToken(token, username);
+        validateJwtToken(refreshToken, username);
+        this.token = token;
+        this.refreshToken = refreshToken;
+        this.username = username;
+    }
+
+    protected void validateJwtToken(String token, String username) {
+        Assert.assertNotNull(token);
+        Assert.assertFalse(token.isEmpty());
+        int i = token.lastIndexOf('.');
+        Assert.assertTrue(i>0);
+        String withoutSignature = token.substring(0, i+1);
+        Jwt<Header,Claims> jwsClaims = Jwts.parser().parseClaimsJwt(withoutSignature);
+        Claims claims = jwsClaims.getBody();
+        String subject = claims.getSubject();
+        Assert.assertEquals(username, subject);
+    }
+    
+    protected void logout() throws Exception {
+        this.token = null;
+        this.refreshToken = null;
+        this.username = null;
+    }
+
+    protected void setJwtToken(MockHttpServletRequestBuilder request) {
+        if (this.token != null) {
+            request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token);
+        }
+    }
+     
+    protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
+        MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
+        setJwtToken(getRequest);
+        return mockMvc.perform(getRequest);
+    }
+    
+    protected <T> T doGet(String urlTemplate, Class<T> responseClass, Object... urlVariables) throws Exception {
+        return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass);
+    }
+    
+    protected <T> T doGetTyped(String urlTemplate, TypeReference<T> responseType, Object... urlVariables) throws Exception {
+        return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType);
+    }
+    
+    protected <T> T doGetTypedWithPageLink(String urlTemplate, TypeReference<T> responseType,
+            TextPageLink pageLink,
+            Object... urlVariables) throws Exception {
+        List<Object> pageLinkVariables = new ArrayList<>();
+        urlTemplate += "limit={limit}";
+        pageLinkVariables.add(pageLink.getLimit());
+        if (StringUtils.isNotEmpty(pageLink.getTextSearch())) {
+            urlTemplate += "&textSearch={textSearch}";
+            pageLinkVariables.add(pageLink.getTextSearch());
+        }
+        if (pageLink.getIdOffset() != null) {
+            urlTemplate += "&idOffset={idOffset}";
+            pageLinkVariables.add(pageLink.getIdOffset().toString());
+        }
+        if (StringUtils.isNotEmpty(pageLink.getTextOffset())) {
+            urlTemplate += "&textOffset={textOffset}";
+            pageLinkVariables.add(pageLink.getTextOffset());
+        }
+        
+        Object[] vars = new Object[urlVariables.length + pageLinkVariables.size()];        
+        System.arraycopy(urlVariables, 0, vars, 0, urlVariables.length);
+        System.arraycopy(pageLinkVariables.toArray(), 0, vars, urlVariables.length, pageLinkVariables.size());
+        
+        return readResponse(doGet(urlTemplate, vars).andExpect(status().isOk()), responseType);
+    }
+    
+    protected <T> T doPost(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
+        return readResponse(doPost(urlTemplate, params).andExpect(status().isOk()), responseClass);
+    }
+    
+    protected <T> T doPost(String urlTemplate, T content, Class<T> responseClass, String... params) throws Exception {
+        return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass);
+    }
+    
+    protected <T> T doDelete(String urlTemplate, Class<T> responseClass, String... params) throws Exception {
+        return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
+    }
+     
+    protected ResultActions doPost(String urlTemplate, String... params) throws Exception {
+        MockHttpServletRequestBuilder postRequest = post(urlTemplate);
+        setJwtToken(postRequest);
+        populateParams(postRequest, params);
+        return mockMvc.perform(postRequest);
+    }
+    
+    protected <T> ResultActions doPost(String urlTemplate, T content, String... params)  throws Exception {
+        MockHttpServletRequestBuilder postRequest = post(urlTemplate);
+        setJwtToken(postRequest);
+        String json = json(content);
+        postRequest.contentType(contentType).content(json);
+        populateParams(postRequest, params);
+        return mockMvc.perform(postRequest);
+    }
+    
+    protected ResultActions doDelete(String urlTemplate, String... params) throws Exception {
+        MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate);
+        setJwtToken(deleteRequest);
+        populateParams(deleteRequest, params);
+        return mockMvc.perform(deleteRequest);
+    }
+    
+    protected void populateParams(MockHttpServletRequestBuilder request, String... params) {
+        if (params != null && params.length > 0) {
+            Assert.assertEquals(params.length % 2, 0);
+            MultiValueMap<String, String> paramsMap = new LinkedMultiValueMap<String, String>();
+            for (int i=0;i<params.length;i+=2) {
+                paramsMap.add(params[i], params[i+1]);
+            }
+            request.params(paramsMap);
+        }
+    }
+    
+    @SuppressWarnings("unchecked")
+    protected String json(Object o) throws IOException {
+        MockHttpOutputMessage mockHttpOutputMessage = new MockHttpOutputMessage();
+        this.mappingJackson2HttpMessageConverter.write(
+                o, MediaType.APPLICATION_JSON, mockHttpOutputMessage);
+        return mockHttpOutputMessage.getBodyAsString();
+    }
+    
+    @SuppressWarnings("unchecked")
+    protected <T> T readResponse(ResultActions result, Class<T> responseClass) throws Exception {
+        byte[] content = result.andReturn().getResponse().getContentAsByteArray();
+        MockHttpInputMessage mockHttpInputMessage = new MockHttpInputMessage(content);
+        return (T) this.mappingJackson2HttpMessageConverter.read(responseClass, mockHttpInputMessage);
+    }
+    
+    protected <T> T readResponse(ResultActions result, TypeReference<T> type) throws Exception {
+        byte[] content = result.andReturn().getResponse().getContentAsByteArray();
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readerFor(type).readValue(content);
+    }
+     
+    class IdComparator<D extends BaseData<? extends UUIDBased>> implements Comparator<D> {
+        @Override
+        public int compare(D o1, D o2) {
+            return o1.getId().getId().compareTo(o2.getId().getId());
+        }
+    }
+
+    protected static <T> ResultMatcher statusReason(Matcher<T> matcher) {
+        return jsonPath("$.message", matcher);
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java
new file mode 100644
index 0000000..6712aa2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AdminControllerTest.java
@@ -0,0 +1,146 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.thingsboard.server.common.data.AdminSettings;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class AdminControllerTest extends AbstractControllerTest {
+
+    @Test
+    public void testFindAdminSettingsByKey() throws Exception {
+        loginSysAdmin();
+        doGet("/api/admin/settings/general")
+        .andExpect(status().isOk())
+        .andExpect(content().contentType(contentType))
+        .andExpect(jsonPath("$.id", notNullValue()))
+        .andExpect(jsonPath("$.key", is("general")))
+        .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://localhost:8080")));
+        
+        doGet("/api/admin/settings/mail")
+        .andExpect(status().isOk())
+        .andExpect(content().contentType(contentType))
+        .andExpect(jsonPath("$.id", notNullValue()))
+        .andExpect(jsonPath("$.key", is("mail")))
+        .andExpect(jsonPath("$.jsonValue.smtpProtocol", is("smtp")))
+        .andExpect(jsonPath("$.jsonValue.smtpHost", is("localhost")))
+        .andExpect(jsonPath("$.jsonValue.smtpPort", is("25")));
+        
+        doGet("/api/admin/settings/unknown")
+        .andExpect(status().isNotFound());
+        
+    }
+    
+    @Test
+    public void testSaveAdminSettings() throws Exception {
+        loginSysAdmin();
+        AdminSettings adminSettings = doGet("/api/admin/settings/general", AdminSettings.class); 
+        
+        JsonNode jsonValue = adminSettings.getJsonValue();
+        ((ObjectNode) jsonValue).put("baseUrl", "http://myhost.org");
+        adminSettings.setJsonValue(jsonValue);
+
+        doPost("/api/admin/settings", adminSettings).andExpect(status().isOk());
+        
+        doGet("/api/admin/settings/general")
+        .andExpect(status().isOk())
+        .andExpect(content().contentType(contentType))
+        .andExpect(jsonPath("$.jsonValue.baseUrl", is("http://myhost.org")));
+        
+        ((ObjectNode) jsonValue).put("baseUrl", "http://localhost:8080");
+        adminSettings.setJsonValue(jsonValue);
+        
+        doPost("/api/admin/settings", adminSettings)
+        .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testCreateAdminSettings() throws Exception {
+        loginSysAdmin();
+        
+        AdminSettings adminSettings = new AdminSettings();
+        adminSettings.setKey("someKey");
+        adminSettings.setJsonValue(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+
+        doPost("/api/admin/settings", adminSettings)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("is prohibited")));
+    }
+
+    @Test
+    public void testSaveAdminSettingsWithEmptyKey() throws Exception {
+        loginSysAdmin();
+        AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); 
+        adminSettings.setKey(null);
+        doPost("/api/admin/settings", adminSettings)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Key should be specified")));
+    }
+    
+    @Test
+    public void testChangeAdminSettingsKey() throws Exception {
+        loginSysAdmin();
+        AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); 
+        adminSettings.setKey("newKey");
+        doPost("/api/admin/settings", adminSettings)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("is prohibited")));
+    }
+    
+    @Test
+    public void testSaveAdminSettingsWithNewJsonStructure() throws Exception {
+        loginSysAdmin();
+        AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); 
+        JsonNode json = adminSettings.getJsonValue();
+        ((ObjectNode) json).put("newKey", "my new value");
+        adminSettings.setJsonValue(json);
+        doPost("/api/admin/settings", adminSettings)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Provided json structure is different")));
+    }
+    
+    @Test
+    public void testSaveAdminSettingsWithNonTextValue() throws Exception {
+        loginSysAdmin();
+        AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class); 
+        JsonNode json = adminSettings.getJsonValue();
+        ((ObjectNode) json).put("timeout", 10000L);
+        adminSettings.setJsonValue(json);
+        doPost("/api/admin/settings", adminSettings)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Provided json structure can't contain non-text values")));
+    }
+    
+    @Test
+    public void testSendTestMail() throws Exception {
+        loginSysAdmin();
+        AdminSettings adminSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
+        doPost("/api/admin/settings/testMail", adminSettings)
+        .andExpect(status().isOk());
+    }
+    
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
new file mode 100644
index 0000000..9fb9fd4
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/AuthControllerTest.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import org.thingsboard.server.common.data.security.Authority;
+import org.junit.Test;
+
+public class AuthControllerTest extends AbstractControllerTest {
+
+    @Test
+    public void testGetUser() throws Exception {
+        
+        doGet("/api/auth/user")
+        .andExpect(status().isUnauthorized());
+        
+        loginSysAdmin();
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+        
+        loginTenantAdmin();
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(TENANT_ADMIN_EMAIL)));
+        
+        loginCustomerUser();
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.CUSTOMER_USER.name())))
+        .andExpect(jsonPath("$.email",is(CUSTOMER_USER_EMAIL)));
+    }
+    
+    @Test
+    public void testLoginLogout() throws Exception {
+        loginSysAdmin();
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+
+        logout();
+        doGet("/api/auth/user")
+        .andExpect(status().isUnauthorized());
+    }
+
+    @Test
+    public void testRefreshToken() throws Exception {
+        loginSysAdmin();
+        doGet("/api/auth/user")
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+                .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+
+        refreshToken();
+        doGet("/api/auth/user")
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.authority",is(Authority.SYS_ADMIN.name())))
+                .andExpect(jsonPath("$.email",is(SYS_ADMIN_EMAIL)));
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java
new file mode 100644
index 0000000..b901966
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/ComponentDescriptorControllerTest.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+import org.thingsboard.server.common.data.plugin.ComponentType;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+
+import java.util.List;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class ComponentDescriptorControllerTest extends AbstractControllerTest {
+
+    private static final int AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS = 5;
+    private Tenant savedTenant;
+    private User tenantAdmin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testGetByClazz() throws Exception {
+        ComponentDescriptor descriptor =
+                doGet("/api/component/" + TelemetryStoragePlugin.class.getName(), ComponentDescriptor.class);
+
+        Assert.assertNotNull(descriptor);
+        Assert.assertNotNull(descriptor.getId());
+        Assert.assertNotNull(descriptor.getName());
+        Assert.assertEquals(ComponentScope.TENANT, descriptor.getScope());
+        Assert.assertEquals(ComponentType.PLUGIN, descriptor.getType());
+        Assert.assertEquals(descriptor.getClazz(), descriptor.getClazz());
+    }
+
+    @Test
+    public void testGetByType() throws Exception {
+        List<ComponentDescriptor> descriptors = readResponse(
+                doGet("/api/components/" + ComponentType.PLUGIN).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
+                });
+
+        Assert.assertNotNull(descriptors);
+        Assert.assertEquals(AMOUNT_OF_DEFAULT_PLUGINS_DESCRIPTORS, descriptors.size());
+
+        for (ComponentType type : ComponentType.values()) {
+            doGet("/api/components/" + type).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testGetActionsByType() throws Exception {
+        List<ComponentDescriptor> descriptors = readResponse(
+                doGet("/api/components/actions/" + TelemetryStoragePlugin.class.getName()).andExpect(status().isOk()), new TypeReference<List<ComponentDescriptor>>() {
+                });
+
+        Assert.assertNotNull(descriptors);
+        Assert.assertEquals(1, descriptors.size());
+        Assert.assertEquals(TelemetryPluginAction.class.getName(), descriptors.get(0).getClazz());
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerTestSuite.java
new file mode 100644
index 0000000..9ac768c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/ControllerTestSuite.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomCassandraCQLUnit;
+
+import java.util.Arrays;
+
+@RunWith(ClasspathSuite.class)
+@ClassnameFilters({"org.thingsboard.server.controller.*Test"})
+public class ControllerTestSuite {
+
+    @ClassRule
+    public static CustomCassandraCQLUnit cassandraUnit =
+            new CustomCassandraCQLUnit(Arrays.asList(
+                                         new ClassPathCQLDataSet("schema.cql", false, false),
+                                         new ClassPathCQLDataSet("system-data.cql", false, false),
+                                         new ClassPathCQLDataSet("system-test.cql", false, false)),
+                    "cassandra-test.yaml", 30000l);
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
new file mode 100644
index 0000000..b71a2d1
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/CustomerControllerTest.java
@@ -0,0 +1,366 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class CustomerControllerTest extends AbstractControllerTest {
+
+    private IdComparator<Customer> idComparator = new IdComparator<>();
+    
+    @Test
+    public void testSaveCustomer() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+        Assert.assertNotNull(savedCustomer);
+        Assert.assertNotNull(savedCustomer.getId());
+        Assert.assertTrue(savedCustomer.getCreatedTime() > 0);
+        Assert.assertEquals(customer.getTitle(), savedCustomer.getTitle());
+        savedCustomer.setTitle("My new customer");
+        doPost("/api/customer", savedCustomer, Customer.class);
+        
+        Customer foundCustomer = doGet("/api/customer/"+savedCustomer.getId().getId().toString(), Customer.class); 
+        Assert.assertEquals(foundCustomer.getTitle(), savedCustomer.getTitle());
+        
+        doDelete("/api/customer/"+savedCustomer.getId().getId().toString())
+        .andExpect(status().isOk());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindCustomerById() throws Exception {
+        
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+        
+        Customer foundCustomer = doGet("/api/customer/"+savedCustomer.getId().getId().toString(), Customer.class);
+        Assert.assertNotNull(foundCustomer);
+        Assert.assertEquals(savedCustomer, foundCustomer);
+        
+        doDelete("/api/customer/"+savedCustomer.getId().getId().toString())
+        .andExpect(status().isOk());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testDeleteCustomer() throws Exception {
+        
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+        
+        doDelete("/api/customer/"+savedCustomer.getId().getId().toString())
+        .andExpect(status().isOk());
+
+        doGet("/api/customer/"+savedCustomer.getId().getId().toString())
+        .andExpect(status().isNotFound());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveCustomerWithEmptyTitle() throws Exception {
+        
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        doPost("/api/customer", customer)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Customer title should be specified")));
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveCustomerWithInvalidEmail() throws Exception {
+        
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        customer.setEmail("invalid@mail");
+        doPost("/api/customer", customer)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Invalid email address format 'invalid@mail'")));
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindCustomers() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        TenantId tenantId = savedTenant.getId();
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(tenantId);
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        List<Customer> customers = new ArrayList<>();
+        for (int i=0;i<135;i++) {
+            Customer customer = new Customer();
+            customer.setTenantId(tenantId);
+            customer.setTitle("Customer"+i);
+            customers.add(doPost("/api/customer", customer, Customer.class));
+        }
+        
+        List<Customer> loadedCustomers = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Customer> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+            loadedCustomers.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(customers, idComparator);
+        Collections.sort(loadedCustomers, idComparator);
+        
+        Assert.assertEquals(customers, loadedCustomers);
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindCustomersByTitle() throws Exception {
+        
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        TenantId tenantId = savedTenant.getId();
+        
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(tenantId);
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        String title1 = "Customer title 1";
+        List<Customer> customersTitle1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Customer customer = new Customer();
+            customer.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title1+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            customer.setTitle(title);
+            customersTitle1.add(doPost("/api/customer", customer, Customer.class));
+        }
+        String title2 = "Customer title 2";
+        List<Customer> customersTitle2 = new ArrayList<>();
+        for (int i=0;i<175;i++) {
+            Customer customer = new Customer();
+            customer.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title2+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            customer.setTitle(title);
+            customersTitle2.add(doPost("/api/customer", customer, Customer.class));
+        }
+        
+        List<Customer> loadedCustomersTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Customer> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+            loadedCustomersTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(customersTitle1, idComparator);
+        Collections.sort(loadedCustomersTitle1, idComparator);
+        
+        Assert.assertEquals(customersTitle1, loadedCustomersTitle1);
+        
+        List<Customer> loadedCustomersTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+            loadedCustomersTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(customersTitle2, idComparator);
+        Collections.sort(loadedCustomersTitle2, idComparator);
+        
+        Assert.assertEquals(customersTitle2, loadedCustomersTitle2);
+        
+        for (Customer customer : loadedCustomersTitle1) {
+            doDelete("/api/customer/"+customer.getId().getId().toString())
+            .andExpect(status().isOk());    
+        }
+        
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (Customer customer : loadedCustomersTitle2) {
+            doDelete("/api/customer/"+customer.getId().getId().toString())
+            .andExpect(status().isOk());    
+        }
+        
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/customers?", new TypeReference<TextPageData<Customer>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
new file mode 100644
index 0000000..7520807
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/DashboardControllerTest.java
@@ -0,0 +1,431 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Dashboard;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class DashboardControllerTest extends AbstractControllerTest {
+    
+    private IdComparator<Dashboard> idComparator = new IdComparator<>();
+    
+    private Tenant savedTenant;
+    private User tenantAdmin;
+    
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+    
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveDashboard() throws Exception {
+        Dashboard dashboard = new Dashboard();
+        dashboard.setTitle("My dashboard");
+        Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+        
+        Assert.assertNotNull(savedDashboard);
+        Assert.assertNotNull(savedDashboard.getId());
+        Assert.assertTrue(savedDashboard.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedDashboard.getTenantId());
+        Assert.assertNotNull(savedDashboard.getCustomerId());
+        Assert.assertEquals(NULL_UUID, savedDashboard.getCustomerId().getId());
+        Assert.assertEquals(dashboard.getTitle(), savedDashboard.getTitle());
+        
+        savedDashboard.setTitle("My new dashboard");
+        doPost("/api/dashboard", savedDashboard, Dashboard.class);
+        
+        Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+        Assert.assertEquals(foundDashboard.getTitle(), savedDashboard.getTitle());
+    }
+    
+    @Test
+    public void testFindDashboardById() throws Exception {
+        Dashboard dashboard = new Dashboard();
+        dashboard.setTitle("My dashboard");
+        Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+        Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+        Assert.assertNotNull(foundDashboard);
+        Assert.assertEquals(savedDashboard, foundDashboard);
+    }
+    
+    @Test
+    public void testDeleteDashboard() throws Exception {
+        Dashboard dashboard = new Dashboard();
+        dashboard.setTitle("My dashboard");
+        Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+        
+        doDelete("/api/dashboard/"+savedDashboard.getId().getId().toString())
+        .andExpect(status().isOk());
+
+        doGet("/api/dashboard/"+savedDashboard.getId().getId().toString())
+        .andExpect(status().isNotFound());
+    }
+    
+    @Test
+    public void testSaveDashboardWithEmptyTitle() throws Exception {
+        Dashboard dashboard = new Dashboard();
+        doPost("/api/dashboard", dashboard)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Dashboard title should be specified")));
+    }
+    
+    @Test
+    public void testAssignUnassignDashboardToCustomer() throws Exception {
+        Dashboard dashboard = new Dashboard();
+        dashboard.setTitle("My dashboard");
+        Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+        
+        Dashboard assignedDashboard = doPost("/api/customer/" + savedCustomer.getId().getId().toString() 
+                + "/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+        Assert.assertEquals(savedCustomer.getId(), assignedDashboard.getCustomerId());
+        
+        Dashboard foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+        Assert.assertEquals(savedCustomer.getId(), foundDashboard.getCustomerId());
+
+        Dashboard unassignedDashboard = 
+                doDelete("/api/customer/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDashboard.getCustomerId().getId());
+        
+        foundDashboard = doGet("/api/dashboard/" + savedDashboard.getId().getId().toString(), Dashboard.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, foundDashboard.getCustomerId().getId());
+    }
+    
+    @Test
+    public void testAssignDashboardToNonExistentCustomer() throws Exception {
+        Dashboard dashboard = new Dashboard();
+        dashboard.setTitle("My dashboard");
+        Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+        
+        doPost("/api/customer/" + UUIDs.timeBased().toString() 
+                + "/dashboard/" + savedDashboard.getId().getId().toString())
+        .andExpect(status().isNotFound());
+    }
+    
+    @Test
+    public void testAssignDashboardToCustomerFromDifferentTenant() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant2 = new Tenant();
+        tenant2.setTitle("Different tenant");
+        Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
+        Assert.assertNotNull(savedTenant2);
+
+        User tenantAdmin2 = new User();
+        tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin2.setTenantId(savedTenant2.getId());
+        tenantAdmin2.setEmail("tenant3@thingsboard.org");
+        tenantAdmin2.setFirstName("Joe");
+        tenantAdmin2.setLastName("Downs");
+        
+        tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("Different customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+        
+        Dashboard dashboard = new Dashboard();
+        dashboard.setTitle("My dashboard");
+        Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+        
+        doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+                + "/dashboard/" + savedDashboard.getId().getId().toString())
+        .andExpect(status().isForbidden());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant2.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testFindTenantDashboards() throws Exception {
+        List<Dashboard> dashboards = new ArrayList<>();
+        for (int i=0;i<173;i++) {
+            Dashboard dashboard = new Dashboard();
+            dashboard.setTitle("Dashboard"+i);
+            dashboards.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+        }
+        List<Dashboard> loadedDashboards = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(24);
+        TextPageData<Dashboard> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", 
+                    new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+            loadedDashboards.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(dashboards, idComparator);
+        Collections.sort(loadedDashboards, idComparator);
+        
+        Assert.assertEquals(dashboards, loadedDashboards);
+    }
+    
+    @Test
+    public void testFindTenantDashboardsByTitle() throws Exception {
+        String title1 = "Dashboard title 1";
+        List<Dashboard> dashboardsTitle1 = new ArrayList<>();
+        for (int i=0;i<134;i++) {
+            Dashboard dashboard = new Dashboard();
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title1+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            dashboard.setTitle(title);
+            dashboardsTitle1.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+        }
+        String title2 = "Dashboard title 2";
+        List<Dashboard> dashboardsTitle2 = new ArrayList<>();
+        for (int i=0;i<112;i++) {
+            Dashboard dashboard = new Dashboard();
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title2+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            dashboard.setTitle(title);
+            dashboardsTitle2.add(doPost("/api/dashboard", dashboard, Dashboard.class));
+        }
+        
+        List<Dashboard> loadedDashboardsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Dashboard> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", 
+                    new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+            loadedDashboardsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(dashboardsTitle1, idComparator);
+        Collections.sort(loadedDashboardsTitle1, idComparator);
+        
+        Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1);
+        
+        List<Dashboard> loadedDashboardsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", 
+                    new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+            loadedDashboardsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(dashboardsTitle2, idComparator);
+        Collections.sort(loadedDashboardsTitle2, idComparator);
+        
+        Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2);
+        
+        for (Dashboard dashboard : loadedDashboardsTitle1) {
+            doDelete("/api/dashboard/"+dashboard.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", 
+                new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (Dashboard dashboard : loadedDashboardsTitle2) {
+            doDelete("/api/dashboard/"+dashboard.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/tenant/dashboards?", 
+                new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+    
+    @Test
+    public void testFindCustomerDashboards() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+        
+        List<Dashboard> dashboards = new ArrayList<>();
+        for (int i=0;i<173;i++) {
+            Dashboard dashboard = new Dashboard();
+            dashboard.setTitle("Dashboard"+i);
+            dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+            dashboards.add(doPost("/api/customer/" + customerId.getId().toString() 
+                            + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class));
+        }
+        
+        List<Dashboard> loadedDashboards = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(21);
+        TextPageData<Dashboard> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", 
+                    new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+            loadedDashboards.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(dashboards, idComparator);
+        Collections.sort(loadedDashboards, idComparator);
+        
+        Assert.assertEquals(dashboards, loadedDashboards);
+    }
+    
+    @Test
+    public void testFindCustomerDashboardsByTitle() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Dashboard title 1";
+        List<Dashboard> dashboardsTitle1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Dashboard dashboard = new Dashboard();
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title1+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            dashboard.setTitle(title);
+            dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+            dashboardsTitle1.add(doPost("/api/customer/" + customerId.getId().toString() 
+                    + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class));
+        }
+        String title2 = "Dashboard title 2";
+        List<Dashboard> dashboardsTitle2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Dashboard dashboard = new Dashboard();
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title2+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            dashboard.setTitle(title);
+            dashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
+            dashboardsTitle2.add(doPost("/api/customer/" + customerId.getId().toString() 
+                    + "/dashboard/" + dashboard.getId().getId().toString(), Dashboard.class));
+        }
+        
+        List<Dashboard> loadedDashboardsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(18, title1);
+        TextPageData<Dashboard> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", 
+                    new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+            loadedDashboardsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(dashboardsTitle1, idComparator);
+        Collections.sort(loadedDashboardsTitle1, idComparator);
+        
+        Assert.assertEquals(dashboardsTitle1, loadedDashboardsTitle1);
+        
+        List<Dashboard> loadedDashboardsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(7, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", 
+                    new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+            loadedDashboardsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(dashboardsTitle2, idComparator);
+        Collections.sort(loadedDashboardsTitle2, idComparator);
+        
+        Assert.assertEquals(dashboardsTitle2, loadedDashboardsTitle2);
+        
+        for (Dashboard dashboard : loadedDashboardsTitle1) {
+            doDelete("/api/customer/dashboard/" + dashboard.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(5, title1);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", 
+                new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (Dashboard dashboard : loadedDashboardsTitle2) {
+            doDelete("/api/customer/dashboard/" + dashboard.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(9, title2);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/dashboards?", 
+                new TypeReference<TextPageData<Dashboard>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
new file mode 100644
index 0000000..50ceb75
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/DeviceControllerTest.java
@@ -0,0 +1,549 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.dao.model.ModelConstants;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.datastax.driver.core.utils.UUIDs;
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class DeviceControllerTest extends AbstractControllerTest {
+    
+    private IdComparator<Device> idComparator = new IdComparator<>();
+    
+    private Tenant savedTenant;
+    private User tenantAdmin;
+    
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+    
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveDevice() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        
+        Assert.assertNotNull(savedDevice);
+        Assert.assertNotNull(savedDevice.getId());
+        Assert.assertTrue(savedDevice.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedDevice.getTenantId());
+        Assert.assertNotNull(savedDevice.getCustomerId());
+        Assert.assertEquals(NULL_UUID, savedDevice.getCustomerId().getId());
+        Assert.assertEquals(device.getName(), savedDevice.getName());
+        
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
+
+        Assert.assertNotNull(deviceCredentials);
+        Assert.assertNotNull(deviceCredentials.getId());
+        Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+        Assert.assertEquals(DeviceCredentialsType.ACCESS_TOKEN, deviceCredentials.getCredentialsType());
+        Assert.assertNotNull(deviceCredentials.getCredentialsId());
+        Assert.assertEquals(20, deviceCredentials.getCredentialsId().length());
+        
+        savedDevice.setName("My new device");
+        doPost("/api/device", savedDevice, Device.class);
+        
+        Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+        Assert.assertEquals(foundDevice.getName(), savedDevice.getName());
+    }
+    
+    @Test
+    public void testFindDeviceById() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+        Assert.assertNotNull(foundDevice);
+        Assert.assertEquals(savedDevice, foundDevice);
+    }
+    
+    @Test
+    public void testDeleteDevice() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        
+        doDelete("/api/device/"+savedDevice.getId().getId().toString())
+        .andExpect(status().isOk());
+
+        doGet("/api/device/"+savedDevice.getId().getId().toString())
+        .andExpect(status().isNotFound());
+    }
+    
+    @Test
+    public void testSaveDeviceWithEmptyName() throws Exception {
+        Device device = new Device();
+        doPost("/api/device", device)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Device name should be specified")));
+    }
+    
+    @Test
+    public void testAssignUnassignDeviceToCustomer() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+        
+        Device assignedDevice = doPost("/api/customer/" + savedCustomer.getId().getId().toString() 
+                + "/device/" + savedDevice.getId().getId().toString(), Device.class);
+        Assert.assertEquals(savedCustomer.getId(), assignedDevice.getCustomerId());
+        
+        Device foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+        Assert.assertEquals(savedCustomer.getId(), foundDevice.getCustomerId());
+
+        Device unassignedDevice = 
+                doDelete("/api/customer/device/" + savedDevice.getId().getId().toString(), Device.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, unassignedDevice.getCustomerId().getId());
+        
+        foundDevice = doGet("/api/device/" + savedDevice.getId().getId().toString(), Device.class);
+        Assert.assertEquals(ModelConstants.NULL_UUID, foundDevice.getCustomerId().getId());
+    }
+    
+    @Test
+    public void testAssignDeviceToNonExistentCustomer() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        
+        doPost("/api/customer/" + UUIDs.timeBased().toString() 
+                + "/device/" + savedDevice.getId().getId().toString())
+        .andExpect(status().isNotFound());
+    }
+    
+    @Test
+    public void testAssignDeviceToCustomerFromDifferentTenant() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant2 = new Tenant();
+        tenant2.setTitle("Different tenant");
+        Tenant savedTenant2 = doPost("/api/tenant", tenant2, Tenant.class);
+        Assert.assertNotNull(savedTenant2);
+
+        User tenantAdmin2 = new User();
+        tenantAdmin2.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin2.setTenantId(savedTenant2.getId());
+        tenantAdmin2.setEmail("tenant3@thingsboard.org");
+        tenantAdmin2.setFirstName("Joe");
+        tenantAdmin2.setLastName("Downs");
+        
+        tenantAdmin2 = createUserAndLogin(tenantAdmin2, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("Different customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+        
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        
+        doPost("/api/customer/" + savedCustomer.getId().getId().toString()
+                + "/device/" + savedDevice.getId().getId().toString())
+        .andExpect(status().isForbidden());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant2.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindDeviceCredentialsByDeviceId() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
+        Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+    }
+    
+    @Test
+    public void testSaveDeviceCredentials() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class); 
+        Assert.assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+        deviceCredentials.setCredentialsType(DeviceCredentialsType.ACCESS_TOKEN);
+        deviceCredentials.setCredentialsId("access_token");
+        doPost("/api/device/credentials", deviceCredentials)
+        .andExpect(status().isOk());
+        
+        DeviceCredentials foundDeviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+        
+        Assert.assertEquals(deviceCredentials, foundDeviceCredentials);
+    }
+    
+    @Test
+    public void testSaveDeviceCredentialsWithEmptyDevice() throws Exception {
+        DeviceCredentials deviceCredentials = new DeviceCredentials();
+        doPost("/api/device/credentials", deviceCredentials)
+        .andExpect(status().isBadRequest());
+    }
+    
+    @Test
+    public void testSaveDeviceCredentialsWithEmptyCredentialsType() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+        deviceCredentials.setCredentialsType(null);
+        doPost("/api/device/credentials", deviceCredentials)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Device credentials type should be specified")));
+    }
+    
+    @Test
+    public void testSaveDeviceCredentialsWithEmptyCredentialsId() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+        deviceCredentials.setCredentialsId(null);
+        doPost("/api/device/credentials", deviceCredentials)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Device credentials id should be specified")));
+    }
+    
+    @Test
+    public void testSaveNonExistentDeviceCredentials() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+        DeviceCredentials newDeviceCredentials = new DeviceCredentials(new DeviceCredentialsId(UUIDs.timeBased()));
+        newDeviceCredentials.setCreatedTime(deviceCredentials.getCreatedTime());
+        newDeviceCredentials.setDeviceId(deviceCredentials.getDeviceId());
+        newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType());
+        newDeviceCredentials.setCredentialsId(deviceCredentials.getCredentialsId());
+        doPost("/api/device/credentials", newDeviceCredentials)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Unable to update non-existent device credentials")));
+    }
+    
+    @Test
+    public void testSaveDeviceCredentialsWithNonExistentDevice() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+        deviceCredentials.setDeviceId(new DeviceId(UUIDs.timeBased()));
+        doPost("/api/device/credentials", deviceCredentials)
+        .andExpect(status().isNotFound());
+    }
+    
+    @Test
+    public void testSaveDeviceCredentialsWithInvalidCredemtialsIdLength() throws Exception {
+        Device device = new Device();
+        device.setName("My device");
+        Device savedDevice = doPost("/api/device", device, Device.class);
+        DeviceCredentials deviceCredentials = 
+                doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+        deviceCredentials.setCredentialsId(RandomStringUtils.randomAlphanumeric(21));
+        doPost("/api/device/credentials", deviceCredentials)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Incorrect access token length")));
+    }
+
+    @Test
+    public void testFindTenantDevices() throws Exception {
+        List<Device> devices = new ArrayList<>();
+        for (int i=0;i<178;i++) {
+            Device device = new Device();
+            device.setName("Device"+i);
+            devices.add(doPost("/api/device", device, Device.class));
+        }
+        List<Device> loadedDevices = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?", 
+                    new TypeReference<TextPageData<Device>>(){}, pageLink);
+            loadedDevices.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(devices, idComparator);
+        Collections.sort(loadedDevices, idComparator);
+        
+        Assert.assertEquals(devices, loadedDevices);
+    }
+    
+    @Test
+    public void testFindTenantDevicesByName() throws Exception {
+        String title1 = "Device title 1";
+        List<Device> devicesTitle1 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            devicesTitle1.add(doPost("/api/device", device, Device.class));
+        }
+        String title2 = "Device title 2";
+        List<Device> devicesTitle2 = new ArrayList<>();
+        for (int i=0;i<75;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            devicesTitle2.add(doPost("/api/device", device, Device.class));
+        }
+        
+        List<Device> loadedDevicesTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?", 
+                    new TypeReference<TextPageData<Device>>(){}, pageLink);
+            loadedDevicesTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(devicesTitle1, idComparator);
+        Collections.sort(loadedDevicesTitle1, idComparator);
+        
+        Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
+        
+        List<Device> loadedDevicesTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/devices?", 
+                    new TypeReference<TextPageData<Device>>(){}, pageLink);
+            loadedDevicesTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesTitle2, idComparator);
+        Collections.sort(loadedDevicesTitle2, idComparator);
+        
+        Assert.assertEquals(devicesTitle2, loadedDevicesTitle2);
+        
+        for (Device device : loadedDevicesTitle1) {
+            doDelete("/api/device/"+device.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/tenant/devices?", 
+                new TypeReference<TextPageData<Device>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (Device device : loadedDevicesTitle2) {
+            doDelete("/api/device/"+device.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/tenant/devices?", 
+                new TypeReference<TextPageData<Device>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+    
+    @Test
+    public void testFindCustomerDevices() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+        
+        List<Device> devices = new ArrayList<>();
+        for (int i=0;i<128;i++) {
+            Device device = new Device();
+            device.setName("Device"+i);
+            device = doPost("/api/device", device, Device.class);
+            devices.add(doPost("/api/customer/" + customerId.getId().toString() 
+                            + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+        
+        List<Device> loadedDevices = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", 
+                    new TypeReference<TextPageData<Device>>(){}, pageLink);
+            loadedDevices.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(devices, idComparator);
+        Collections.sort(loadedDevices, idComparator);
+        
+        Assert.assertEquals(devices, loadedDevices);
+    }
+    
+    @Test
+    public void testFindCustomerDevicesByName() throws Exception {
+        Customer customer = new Customer();
+        customer.setTitle("Test customer");
+        customer = doPost("/api/customer", customer, Customer.class);
+        CustomerId customerId = customer.getId();
+
+        String title1 = "Device title 1";
+        List<Device> devicesTitle1 = new ArrayList<>();
+        for (int i=0;i<125;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title1+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device = doPost("/api/device", device, Device.class);
+            devicesTitle1.add(doPost("/api/customer/" + customerId.getId().toString() 
+                    + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+        String title2 = "Device title 2";
+        List<Device> devicesTitle2 = new ArrayList<>();
+        for (int i=0;i<143;i++) {
+            Device device = new Device();
+            String suffix = RandomStringUtils.randomAlphanumeric(15);
+            String name = title2+suffix;
+            name = i % 2 == 0 ? name.toLowerCase() : name.toUpperCase();
+            device.setName(name);
+            device = doPost("/api/device", device, Device.class);
+            devicesTitle2.add(doPost("/api/customer/" + customerId.getId().toString() 
+                    + "/device/" + device.getId().getId().toString(), Device.class));
+        }
+        
+        List<Device> loadedDevicesTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Device> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", 
+                    new TypeReference<TextPageData<Device>>(){}, pageLink);
+            loadedDevicesTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(devicesTitle1, idComparator);
+        Collections.sort(loadedDevicesTitle1, idComparator);
+        
+        Assert.assertEquals(devicesTitle1, loadedDevicesTitle1);
+        
+        List<Device> loadedDevicesTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", 
+                    new TypeReference<TextPageData<Device>>(){}, pageLink);
+            loadedDevicesTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(devicesTitle2, idComparator);
+        Collections.sort(loadedDevicesTitle2, idComparator);
+        
+        Assert.assertEquals(devicesTitle2, loadedDevicesTitle2);
+        
+        for (Device device : loadedDevicesTitle1) {
+            doDelete("/api/customer/device/" + device.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", 
+                new TypeReference<TextPageData<Device>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (Device device : loadedDevicesTitle2) {
+            doDelete("/api/customer/device/" + device.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/devices?", 
+                new TypeReference<TextPageData<Device>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/PluginControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/PluginControllerTest.java
new file mode 100644
index 0000000..5d978e3
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/PluginControllerTest.java
@@ -0,0 +1,232 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class PluginControllerTest extends AbstractControllerTest {
+
+    private IdComparator<PluginMetaData> idComparator = new IdComparator<>();
+
+    private final ObjectMapper mapper = new ObjectMapper();
+    private Tenant savedTenant;
+    private User tenantAdmin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSavePlugin() throws Exception {
+        PluginMetaData plugin = new PluginMetaData();
+        doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+        plugin.setName("My plugin");
+        doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+        plugin.setApiToken("myplugin");
+        doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+        plugin.setConfiguration(mapper.readTree("{}"));
+        doPost("/api/plugin", plugin).andExpect(status().isBadRequest());
+        plugin.setClazz(TelemetryStoragePlugin.class.getName());
+        PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+        Assert.assertNotNull(savedPlugin);
+        Assert.assertNotNull(savedPlugin.getId());
+        Assert.assertTrue(savedPlugin.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedPlugin.getTenantId());
+    }
+
+    @Test
+    public void testFindPluginById() throws Exception {
+        PluginMetaData plugin = new PluginMetaData();
+        plugin.setName("My plugin");
+        plugin.setApiToken("myplugin");
+        plugin.setConfiguration(mapper.readTree("{}"));
+        plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+        PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+        PluginMetaData foundPlugin = doGet("/api/plugin/" + savedPlugin.getId().getId().toString(), PluginMetaData.class);
+        Assert.assertNotNull(foundPlugin);
+        Assert.assertEquals(savedPlugin, foundPlugin);
+    }
+
+    @Test
+    public void testActivatePlugin() throws Exception {
+        PluginMetaData plugin = new PluginMetaData();
+        plugin.setName("My plugin");
+        plugin.setApiToken("myplugin");
+        plugin.setConfiguration(mapper.readTree("{}"));
+        plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+        PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+        doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSuspendPlugin() throws Exception {
+        PluginMetaData plugin = new PluginMetaData();
+        plugin.setName("My plugin");
+        plugin.setApiToken("myplugin");
+        plugin.setConfiguration(mapper.readTree("{}"));
+        plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+        PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+        doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+
+        RuleMetaData rule = RuleControllerTest.createRuleMetaData(savedPlugin);
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+        doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isOk());
+
+        doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/suspend").andExpect(status().isBadRequest());
+
+        doPost("/api/rule/" + savedRule.getId().getId().toString() + "/suspend").andExpect(status().isOk());
+
+        doPost("/api/plugin/" + savedPlugin.getId().getId().toString() + "/suspend").andExpect(status().isOk());
+    }
+
+    @Test
+    public void testDeletePluginById() throws Exception {
+        PluginMetaData plugin = new PluginMetaData();
+        plugin.setName("My plugin");
+        plugin.setApiToken("myplugin");
+        plugin.setConfiguration(mapper.readTree("{}"));
+        plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+        PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+
+        RuleMetaData rule = RuleControllerTest.createRuleMetaData(savedPlugin);
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+        doDelete("/api/plugin/" + savedPlugin.getId().getId()).andExpect(status().isBadRequest());
+
+        doDelete("/api/rule/" + savedRule.getId().getId()).andExpect(status().isOk());
+
+        doDelete("/api/plugin/" + savedPlugin.getId().getId()).andExpect(status().isOk());
+        doGet("/api/plugin/" + savedPlugin.getId().getId().toString()).andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testFindPluginByToken() throws Exception {
+        PluginMetaData plugin = new PluginMetaData();
+        plugin.setName("My plugin");
+        plugin.setApiToken("myplugin");
+        plugin.setConfiguration(mapper.readTree("{}"));
+        plugin.setClazz(TelemetryStoragePlugin.class.getName());
+
+        PluginMetaData savedPlugin = doPost("/api/plugin", plugin, PluginMetaData.class);
+        PluginMetaData foundPlugin = doGet("/api/plugin/token/" + "myplugin", PluginMetaData.class);
+        Assert.assertNotNull(foundPlugin);
+        Assert.assertEquals(savedPlugin, foundPlugin);
+    }
+
+    @Test
+    public void testFindCurrentTenantPlugins() throws Exception {
+        List<PluginMetaData> plugins = testPluginsCreation("/api/plugin");
+        for (PluginMetaData plugin : plugins) {
+            doDelete("/api/plugin/" + plugin.getId().getId()).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testFindSystemPlugins() throws Exception {
+        loginSysAdmin();
+        List<PluginMetaData> plugins = testPluginsCreation("/api/plugin/system");
+        for (PluginMetaData plugin : plugins) {
+            doDelete("/api/plugin/" + plugin.getId().getId()).andExpect(status().isOk());
+        }
+    }
+
+    private List<PluginMetaData> testPluginsCreation(String url) throws Exception {
+        List<PluginMetaData> plugins = new ArrayList<>();
+        for (int i = 0; i < 111; i++) {
+            PluginMetaData plugin = new PluginMetaData();
+            plugin.setName("My plugin");
+            plugin.setApiToken("myplugin" + i);
+            plugin.setConfiguration(mapper.readTree("{}"));
+            plugin.setClazz(TelemetryStoragePlugin.class.getName());
+            plugins.add(doPost("/api/plugin", plugin, PluginMetaData.class));
+        }
+
+        List<PluginMetaData> loadedPlugins = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(23);
+        TextPageData<PluginMetaData> pageData;
+        do {
+            pageData = doGetTypedWithPageLink(url + "?",
+                    new TypeReference<TextPageData<PluginMetaData>>() {
+                    }, pageLink);
+            loadedPlugins.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        loadedPlugins = loadedPlugins.stream()
+                .filter(p -> !p.getName().equals("System Telemetry Plugin"))
+                .filter(p -> !p.getName().equals("Mail Sender Plugin"))
+                .filter(p -> !p.getName().equals("System RPC Plugin"))
+                .collect(Collectors.toList());
+
+        Collections.sort(plugins, idComparator);
+        Collections.sort(loadedPlugins, idComparator);
+
+        Assert.assertEquals(plugins, loadedPlugins);
+        return loadedPlugins;
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/RuleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/RuleControllerTest.java
new file mode 100644
index 0000000..ddbc833
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/RuleControllerTest.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.plugin.PluginMetaData;
+import org.thingsboard.server.common.data.rule.RuleMetaData;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TelemetryStoragePlugin;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class RuleControllerTest extends AbstractControllerTest {
+
+    private IdComparator<RuleMetaData> idComparator = new IdComparator<>();
+
+    private static final ObjectMapper mapper = new ObjectMapper();
+    private Tenant savedTenant;
+    private User tenantAdmin;
+    private PluginMetaData sysPlugin;
+    private PluginMetaData tenantPlugin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        sysPlugin = new PluginMetaData();
+        sysPlugin.setName("Sys plugin");
+        sysPlugin.setApiToken("sysplugin");
+        sysPlugin.setConfiguration(mapper.readTree("{}"));
+        sysPlugin.setClazz(TelemetryStoragePlugin.class.getName());
+        sysPlugin = doPost("/api/plugin", sysPlugin, PluginMetaData.class);
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+        tenantPlugin = new PluginMetaData();
+        tenantPlugin.setName("My plugin");
+        tenantPlugin.setApiToken("myplugin");
+        tenantPlugin.setConfiguration(mapper.readTree("{}"));
+        tenantPlugin.setClazz(TelemetryStoragePlugin.class.getName());
+        tenantPlugin = doPost("/api/plugin", tenantPlugin, PluginMetaData.class);
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+
+        doDelete("/api/plugin/" + sysPlugin.getId().getId()).andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSaveRule() throws Exception {
+        RuleMetaData rule = new RuleMetaData();
+        doPost("/api/rule", rule).andExpect(status().isBadRequest());
+        rule.setName("My Rule");
+        doPost("/api/rule", rule).andExpect(status().isBadRequest());
+        rule.setPluginToken(tenantPlugin.getApiToken());
+        doPost("/api/rule", rule).andExpect(status().isBadRequest());
+        rule.setFilters(mapper.readTree("[{\"clazz\":\"org.thingsboard.server.extensions.core.filter.MsgTypeFilter\", " +
+                "\"name\":\"TelemetryFilter\", " +
+                "\"configuration\": {\"messageTypes\":[\"POST_TELEMETRY\",\"POST_ATTRIBUTES\",\"GET_ATTRIBUTES\"]}}]"));
+        doPost("/api/rule", rule).andExpect(status().isBadRequest());
+        rule.setAction(mapper.readTree("{\"clazz\":\"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction\", \"name\":\"TelemetryMsgConverterAction\", \"configuration\":{}}"));
+
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+        Assert.assertNotNull(savedRule);
+        Assert.assertNotNull(savedRule.getId());
+        Assert.assertTrue(savedRule.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedRule.getTenantId());
+    }
+
+    @Test
+    public void testFindRuleById() throws Exception {
+        RuleMetaData rule = createRuleMetaData(tenantPlugin);
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+        RuleMetaData foundRule = doGet("/api/rule/" + savedRule.getId().getId().toString(), RuleMetaData.class);
+        Assert.assertNotNull(foundRule);
+        Assert.assertEquals(savedRule, foundRule);
+    }
+
+    @Test
+    public void testFindRuleByPluginToken() throws Exception {
+        RuleMetaData rule = createRuleMetaData(tenantPlugin);
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+        List<RuleMetaData> foundRules = doGetTyped("/api/rule/token/" + savedRule.getPluginToken(),
+                new TypeReference<List<RuleMetaData>>() {
+                });
+        Assert.assertNotNull(foundRules);
+        Assert.assertEquals(1, foundRules.size());
+        Assert.assertEquals(savedRule, foundRules.get(0));
+    }
+
+    @Test
+    public void testActivateRule() throws Exception {
+        RuleMetaData rule = createRuleMetaData(tenantPlugin);
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+        doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isBadRequest());
+
+        doPost("/api/plugin/" + tenantPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+
+        doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSuspendRule() throws Exception {
+        RuleMetaData rule = createRuleMetaData(tenantPlugin);
+        RuleMetaData savedRule = doPost("/api/rule", rule, RuleMetaData.class);
+
+        doPost("/api/plugin/" + tenantPlugin.getId().getId().toString() + "/activate").andExpect(status().isOk());
+        doPost("/api/rule/" + savedRule.getId().getId().toString() + "/activate").andExpect(status().isOk());
+        doPost("/api/rule/" + savedRule.getId().getId().toString() + "/suspend").andExpect(status().isOk());
+    }
+
+    @Test
+    public void testFindSystemRules() throws Exception {
+        loginSysAdmin();
+        List<RuleMetaData> rules = testRulesCreation("/api/rule/system", sysPlugin);
+        for (RuleMetaData rule : rules) {
+            doDelete("/api/rule/" + rule.getId().getId()).andExpect(status().isOk());
+        }
+        loginTenantAdmin();
+    }
+
+    @Test
+    public void testFindCurrentTenantPlugins() throws Exception {
+        List<RuleMetaData> rules = testRulesCreation("/api/rule", tenantPlugin);
+        for (RuleMetaData rule : rules) {
+            doDelete("/api/rule/" + rule.getId().getId()).andExpect(status().isOk());
+        }
+    }
+
+    @Test
+    public void testFindTenantPlugins() throws Exception {
+        List<RuleMetaData> rules = testRulesCreation("/api/rule", tenantPlugin);
+        loginSysAdmin();
+        List<RuleMetaData> loadedRules = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(3);
+        TextPageData<RuleMetaData> pageData;
+        do {
+            pageData = doGetTypedWithPageLink("/api/rule/tenant/" + savedTenant.getId().getId().toString() + "?",
+                    new TypeReference<TextPageData<RuleMetaData>>() {
+                    }, pageLink);
+            loadedRules.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(rules, idComparator);
+        Collections.sort(loadedRules, idComparator);
+
+        Assert.assertEquals(rules, loadedRules);
+
+        for (RuleMetaData rule : rules) {
+            doDelete("/api/rule/" + rule.getId().getId()).andExpect(status().isOk());
+        }
+    }
+
+    private List<RuleMetaData> testRulesCreation(String url, PluginMetaData plugin) throws Exception {
+        List<RuleMetaData> rules = new ArrayList<>();
+        for (int i = 0; i < 6; i++) {
+            RuleMetaData rule = createRuleMetaData(plugin);
+            rule.setPluginToken(plugin.getApiToken());
+            rule.setName(rule.getName() + i);
+            rules.add(doPost("/api/rule", rule, RuleMetaData.class));
+        }
+
+        List<RuleMetaData> loadedRules = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(3);
+        TextPageData<RuleMetaData> pageData;
+        do {
+            pageData = doGetTypedWithPageLink(url + "?",
+                    new TypeReference<TextPageData<RuleMetaData>>() {
+                    }, pageLink);
+            loadedRules.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        loadedRules = loadedRules.stream().filter(p -> !p.getName().equals("System Telemetry Rule")).collect(Collectors.toList());
+
+        Collections.sort(rules, idComparator);
+        Collections.sort(loadedRules, idComparator);
+
+        Assert.assertEquals(rules, loadedRules);
+        return loadedRules;
+    }
+
+    public static RuleMetaData createRuleMetaData(PluginMetaData plugin) throws IOException {
+        RuleMetaData rule = new RuleMetaData();
+        rule.setName("My Rule");
+        rule.setPluginToken(plugin.getApiToken());
+        rule.setFilters(mapper.readTree("[{\"clazz\":\"org.thingsboard.server.extensions.core.filter.MsgTypeFilter\", " +
+                "\"name\":\"TelemetryFilter\", " +
+                "\"configuration\": {\"messageTypes\":[\"POST_TELEMETRY\",\"POST_ATTRIBUTES\",\"GET_ATTRIBUTES\"]}}]"));
+        rule.setAction(mapper.readTree("{\"clazz\":\"org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction\", \"name\":\"TelemetryMsgConverterAction\", \"configuration\":{}}"));
+        return rule;
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
new file mode 100644
index 0000000..03f2d69
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/TenantControllerTest.java
@@ -0,0 +1,220 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.junit.Assert;
+import org.junit.Test;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+
+public class TenantControllerTest extends AbstractControllerTest {
+    
+    private IdComparator<Tenant> idComparator = new IdComparator<>();
+
+    @Test
+    public void testSaveTenant() throws Exception {
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        Assert.assertNotNull(savedTenant.getId());
+        Assert.assertTrue(savedTenant.getCreatedTime() > 0);
+        Assert.assertEquals(tenant.getTitle(), savedTenant.getTitle());
+        savedTenant.setTitle("My new tenant");
+        doPost("/api/tenant", savedTenant, Tenant.class);
+        Tenant foundTenant = doGet("/api/tenant/"+savedTenant.getId().getId().toString(), Tenant.class); 
+        Assert.assertEquals(foundTenant.getTitle(), savedTenant.getTitle());
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindTenantById() throws Exception {
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Tenant foundTenant = doGet("/api/tenant/"+savedTenant.getId().getId().toString(), Tenant.class); 
+        Assert.assertNotNull(foundTenant);
+        Assert.assertEquals(savedTenant, foundTenant);
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveTenantWithEmptyTitle() throws Exception {
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        doPost("/api/tenant", tenant)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Tenant title should be specified")));
+    }
+    
+    @Test
+    public void testSaveTenantWithInvalidEmail() throws Exception {
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        tenant.setEmail("invalid@mail");
+        doPost("/api/tenant", tenant)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Invalid email address format")));
+    }
+    
+    @Test
+    public void testDeleteTenant() throws Exception {
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());        
+        doGet("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isNotFound());
+    }
+    
+    @Test
+    public void testFindTenants() throws Exception {
+        loginSysAdmin();
+        List<Tenant> tenants = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(17);
+        TextPageData<Tenant> pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(1, pageData.getData().size());
+        tenants.addAll(pageData.getData());
+        
+        for (int i=0;i<56;i++) {
+            Tenant tenant = new Tenant();
+            tenant.setTitle("Tenant"+i);
+            tenants.add(doPost("/api/tenant", tenant, Tenant.class));
+        }
+        
+        List<Tenant> loadedTenants = new ArrayList<>();
+        pageLink = new TextPageLink(17);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+            loadedTenants.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(tenants, idComparator);
+        Collections.sort(loadedTenants, idComparator);
+        
+        Assert.assertEquals(tenants, loadedTenants);
+        
+        for (Tenant tenant : loadedTenants) {
+            if (!tenant.getTitle().equals("Tenant")) {
+                doDelete("/api/tenant/"+tenant.getId().getId().toString())
+                .andExpect(status().isOk());        
+            }
+        }
+        
+        pageLink = new TextPageLink(17);
+        pageData =  doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(1, pageData.getData().size());
+    }
+    
+    @Test
+    public void testFindTenantsByTitle() throws Exception {
+        loginSysAdmin();
+        String title1 = "Tenant title 1";
+        List<Tenant> tenantsTitle1 = new ArrayList<>();
+        for (int i=0;i<134;i++) {
+            Tenant tenant = new Tenant();
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title1+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            tenant.setTitle(title);
+            tenantsTitle1.add(doPost("/api/tenant", tenant, Tenant.class));
+        }
+        String title2 = "Tenant title 2";
+        List<Tenant> tenantsTitle2 = new ArrayList<>();
+        for (int i=0;i<127;i++) {
+            Tenant tenant = new Tenant();
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(Math.random()*15));
+            String title = title2+suffix;
+            title = i % 2 == 0 ? title.toLowerCase() : title.toUpperCase();
+            tenant.setTitle(title);
+            tenantsTitle2.add(doPost("/api/tenant", tenant, Tenant.class));
+        }
+        
+        List<Tenant> loadedTenantsTitle1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(15, title1);
+        TextPageData<Tenant> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+            loadedTenantsTitle1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(tenantsTitle1, idComparator);
+        Collections.sort(loadedTenantsTitle1, idComparator);
+        
+        Assert.assertEquals(tenantsTitle1, loadedTenantsTitle1);
+        
+        List<Tenant> loadedTenantsTitle2 = new ArrayList<>();
+        pageLink = new TextPageLink(4, title2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+            loadedTenantsTitle2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(tenantsTitle2, idComparator);
+        Collections.sort(loadedTenantsTitle2, idComparator);
+        
+        Assert.assertEquals(tenantsTitle2, loadedTenantsTitle2);
+
+        for (Tenant tenant : loadedTenantsTitle1) {
+            doDelete("/api/tenant/"+tenant.getId().getId().toString())
+            .andExpect(status().isOk());        
+        }
+        
+        pageLink = new TextPageLink(4, title1);
+        pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (Tenant tenant : loadedTenantsTitle2) {
+            doDelete("/api/tenant/"+tenant.getId().getId().toString())
+            .andExpect(status().isOk());     
+        }
+        
+        pageLink = new TextPageLink(4, title2);
+        pageData = doGetTypedWithPageLink("/api/tenants?", new TypeReference<TextPageData<Tenant>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+    }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
new file mode 100644
index 0000000..a48630c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/UserControllerTest.java
@@ -0,0 +1,618 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.Assert;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.service.mail.TestMailService;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.is;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+public class UserControllerTest extends AbstractControllerTest {
+    
+    private IdComparator<User> idComparator = new IdComparator<>();
+
+    @Test
+    public void testSaveUser() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        String email = "tenant2@thingsboard.org";
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setEmail(email);
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        User savedUser = doPost("/api/user", user, User.class);
+        Assert.assertNotNull(savedUser);
+        Assert.assertNotNull(savedUser.getId());
+        Assert.assertTrue(savedUser.getCreatedTime() > 0);
+        Assert.assertEquals(user.getEmail(), savedUser.getEmail());
+
+        User foundUser = doGet("/api/user/"+savedUser.getId().getId().toString(), User.class); 
+        Assert.assertEquals(foundUser, savedUser);
+        
+        logout();
+        doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
+        .andExpect(status().isPermanentRedirect())
+        .andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
+
+        JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", "activateToken", TestMailService.currentActivateToken, "password", "testPassword").andExpect(status().isOk()), JsonNode.class);
+        validateAndSetJwtToken(tokenInfo, email);
+
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(email)));
+        
+        logout();
+        
+        login(email, "testPassword");
+        
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(email)));
+        
+        loginSysAdmin();
+        doDelete("/api/user/"+savedUser.getId().getId().toString())
+        .andExpect(status().isOk());
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testResetPassword() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        String email = "tenant2@thingsboard.org";
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setEmail(email);
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        User savedUser = createUserAndLogin(user, "testPassword1");
+        logout();
+        doPost("/api/noauth/resetPasswordByEmail", "email", email)
+        .andExpect(status().isOk());
+        doGet("/api/noauth/resetPassword?resetToken={resetToken}", TestMailService.currentResetPasswordToken)
+        .andExpect(status().isPermanentRedirect())
+        .andExpect(header().string(HttpHeaders.LOCATION, "/login/resetPassword?resetToken=" + TestMailService.currentResetPasswordToken));
+        
+        JsonNode tokenInfo = readResponse(doPost("/api/noauth/resetPassword", "resetToken", TestMailService.currentResetPasswordToken, "password", "testPassword2").andExpect(status().isOk()), JsonNode.class);
+        validateAndSetJwtToken(tokenInfo, email);
+
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(email)));
+        
+        logout();
+        
+        login(email, "testPassword2");
+        doGet("/api/auth/user")
+        .andExpect(status().isOk())
+        .andExpect(jsonPath("$.authority",is(Authority.TENANT_ADMIN.name())))
+        .andExpect(jsonPath("$.email",is(email)));
+        
+        loginSysAdmin();
+        doDelete("/api/user/"+savedUser.getId().getId().toString())
+        .andExpect(status().isOk());
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindUserById() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        String email = "tenant2@thingsboard.org";
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setEmail(email);
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        User savedUser = doPost("/api/user", user, User.class);
+        User foundUser = doGet("/api/user/"+savedUser.getId().getId().toString(), User.class); 
+        Assert.assertNotNull(foundUser);
+        Assert.assertEquals(savedUser, foundUser);
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveUserWithSameEmail() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        String email = "tenant@thingsboard.org";
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setEmail(email);
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        doPost("/api/user", user)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("User with email '" + email + "'  already present in database")));
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveUserWithInvalidEmail() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        String email = "tenant_thingsboard.org";
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setEmail(email);
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        doPost("/api/user", user)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Invalid email address format '" + email + "'")));
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveUserWithEmptyEmail() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        doPost("/api/user", user)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("User email should be specified")));
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testSaveUserWithoutTenant() throws Exception {
+        loginSysAdmin();
+        
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setEmail("tenant2@thingsboard.org");
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        doPost("/api/user", user)
+        .andExpect(status().isBadRequest())
+        .andExpect(statusReason(containsString("Tenant administrator should be assigned to tenant")));
+    }
+    
+    @Test
+    public void testDeleteUser() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        String email = "tenant2@thingsboard.org";
+        User user = new User();
+        user.setAuthority(Authority.TENANT_ADMIN);
+        user.setTenantId(savedTenant.getId());
+        user.setEmail(email);
+        user.setFirstName("Joe");
+        user.setLastName("Downs");
+        
+        User savedUser = doPost("/api/user", user, User.class);
+        User foundUser = doGet("/api/user/"+savedUser.getId().getId().toString(), User.class); 
+        Assert.assertNotNull(foundUser);
+        
+        doDelete("/api/user/"+savedUser.getId().getId().toString())
+        .andExpect(status().isOk());
+        
+        doGet("/api/user/"+savedUser.getId().getId().toString())
+        .andExpect(status().isNotFound());
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindTenantAdmins() throws Exception {
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        TenantId tenantId = savedTenant.getId();
+        
+        List<User> tenantAdmins = new ArrayList<>();
+        for (int i=0;i<64;i++) {
+            User user = new User();
+            user.setAuthority(Authority.TENANT_ADMIN);
+            user.setTenantId(tenantId);
+            user.setEmail("testTenant" + i + "@thingsboard.org");
+            tenantAdmins.add(doPost("/api/user", user, User.class));
+        }
+        
+        List<User> loadedTenantAdmins = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(33);
+        TextPageData<User> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", 
+                    new TypeReference<TextPageData<User>>(){}, pageLink);
+            loadedTenantAdmins.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(tenantAdmins, idComparator);
+        Collections.sort(loadedTenantAdmins, idComparator);
+        
+        Assert.assertEquals(tenantAdmins, loadedTenantAdmins);
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+        
+        pageLink = new TextPageLink(33);
+        pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", 
+                new TypeReference<TextPageData<User>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertTrue(pageData.getData().isEmpty());
+    }
+    
+    @Test
+    public void testFindTenantAdminsByEmail() throws Exception {
+        
+        loginSysAdmin();
+        
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        TenantId tenantId = savedTenant.getId();
+        
+        String email1 = "testEmail1";      
+        List<User> tenantAdminsEmail1 = new ArrayList<>();
+        
+        for (int i=0;i<124;i++) {
+            User user = new User();
+            user.setAuthority(Authority.TENANT_ADMIN);
+            user.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+            String email = email1+suffix+ "@thingsboard.org";
+            email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+            user.setEmail(email);
+            tenantAdminsEmail1.add(doPost("/api/user", user, User.class));
+        }
+        
+        String email2 = "testEmail2";        
+        List<User> tenantAdminsEmail2 = new ArrayList<>();
+        
+        for (int i=0;i<112;i++) {
+            User user = new User();
+            user.setAuthority(Authority.TENANT_ADMIN);
+            user.setTenantId(tenantId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+            String email = email2+suffix+ "@thingsboard.org";
+            email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+            user.setEmail(email);
+            tenantAdminsEmail2.add(doPost("/api/user", user, User.class));
+        }
+        
+        List<User> loadedTenantAdminsEmail1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(33, email1);
+        TextPageData<User> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", 
+                    new TypeReference<TextPageData<User>>(){}, pageLink);
+            loadedTenantAdminsEmail1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(tenantAdminsEmail1, idComparator);
+        Collections.sort(loadedTenantAdminsEmail1, idComparator);
+        
+        Assert.assertEquals(tenantAdminsEmail1, loadedTenantAdminsEmail1);
+        
+        List<User> loadedTenantAdminsEmail2 = new ArrayList<>();
+        pageLink = new TextPageLink(16, email2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", 
+                    new TypeReference<TextPageData<User>>(){}, pageLink);
+            loadedTenantAdminsEmail2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(tenantAdminsEmail2, idComparator);
+        Collections.sort(loadedTenantAdminsEmail2, idComparator);
+        
+        Assert.assertEquals(tenantAdminsEmail2, loadedTenantAdminsEmail2);
+        
+        for (User user : loadedTenantAdminsEmail1) {
+            doDelete("/api/user/"+user.getId().getId().toString())
+                .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, email1);
+        pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", 
+                new TypeReference<TextPageData<User>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (User user : loadedTenantAdminsEmail2) {
+            doDelete("/api/user/"+user.getId().getId().toString())
+                .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, email2);
+        pageData = doGetTypedWithPageLink("/api/tenant/" + tenantId.getId().toString() + "/users?", 
+                new TypeReference<TextPageData<User>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindCustomerUsers() throws Exception {
+        
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        TenantId tenantId = savedTenant.getId();
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(tenantId);
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        CustomerId customerId = savedCustomer.getId();
+        
+        List<User> customerUsers = new ArrayList<>();
+        for (int i=0;i<56;i++) {
+            User user = new User();
+            user.setAuthority(Authority.CUSTOMER_USER);
+            user.setCustomerId(customerId);
+            user.setEmail("testCustomer" + i + "@thingsboard.org");
+            customerUsers.add(doPost("/api/user", user, User.class));
+        }
+        
+        List<User> loadedCustomerUsers = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(33);
+        TextPageData<User> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", 
+                    new TypeReference<TextPageData<User>>(){}, pageLink);
+            loadedCustomerUsers.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(customerUsers, idComparator);
+        Collections.sort(loadedCustomerUsers, idComparator);
+        
+        Assert.assertEquals(customerUsers, loadedCustomerUsers);
+        
+        doDelete("/api/customer/"+customerId.getId().toString())
+        .andExpect(status().isOk());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+    @Test
+    public void testFindCustomerUsersByEmail() throws Exception {
+        
+        loginSysAdmin();
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+        
+        TenantId tenantId = savedTenant.getId();
+        User tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(tenantId);
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+        
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+        
+        Customer customer = new Customer();
+        customer.setTitle("My customer");
+        Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
+
+        CustomerId customerId = savedCustomer.getId();
+        
+        String email1 = "testEmail1";        
+        List<User> customerUsersEmail1 = new ArrayList<>();
+        
+        for (int i=0;i<74;i++) {
+            User user = new User();
+            user.setAuthority(Authority.CUSTOMER_USER);
+            user.setCustomerId(customerId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+            String email = email1+suffix+ "@thingsboard.org";
+            email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+            user.setEmail(email);
+            customerUsersEmail1.add(doPost("/api/user", user, User.class));
+        }
+        
+        String email2 = "testEmail2";        
+        List<User> customerUsersEmail2 = new ArrayList<>();
+        
+        for (int i=0;i<92;i++) {
+            User user = new User();
+            user.setAuthority(Authority.CUSTOMER_USER);
+            user.setCustomerId(customerId);
+            String suffix = RandomStringUtils.randomAlphanumeric((int)(5 + Math.random()*10));
+            String email = email2+suffix+ "@thingsboard.org";
+            email = i % 2 == 0 ? email.toLowerCase() : email.toUpperCase();
+            user.setEmail(email);
+            customerUsersEmail2.add(doPost("/api/user", user, User.class));
+        }
+        
+        List<User> loadedCustomerUsersEmail1 = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(33, email1);
+        TextPageData<User> pageData = null;
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", 
+                    new TypeReference<TextPageData<User>>(){}, pageLink);
+            loadedCustomerUsersEmail1.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(customerUsersEmail1, idComparator);
+        Collections.sort(loadedCustomerUsersEmail1, idComparator);
+        
+        Assert.assertEquals(customerUsersEmail1, loadedCustomerUsersEmail1);
+        
+        List<User> loadedCustomerUsersEmail2 = new ArrayList<>();
+        pageLink = new TextPageLink(16, email2);
+        do {
+            pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", 
+                    new TypeReference<TextPageData<User>>(){}, pageLink);
+            loadedCustomerUsersEmail2.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+        
+        Collections.sort(customerUsersEmail2, idComparator);
+        Collections.sort(loadedCustomerUsersEmail2, idComparator);
+        
+        Assert.assertEquals(customerUsersEmail2, loadedCustomerUsersEmail2);
+        
+        for (User user : loadedCustomerUsersEmail1) {
+            doDelete("/api/user/"+user.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, email1);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", 
+                new TypeReference<TextPageData<User>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        for (User user : loadedCustomerUsersEmail2) {
+            doDelete("/api/user/"+user.getId().getId().toString())
+            .andExpect(status().isOk());
+        }
+        
+        pageLink = new TextPageLink(4, email2);
+        pageData = doGetTypedWithPageLink("/api/customer/" + customerId.getId().toString() + "/users?", 
+                new TypeReference<TextPageData<User>>(){}, pageLink);
+        Assert.assertFalse(pageData.hasNext());
+        Assert.assertEquals(0, pageData.getData().size());
+        
+        doDelete("/api/customer/"+customerId.getId().toString())
+        .andExpect(status().isOk());
+        
+        loginSysAdmin();
+        
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+        .andExpect(status().isOk());
+    }
+    
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
new file mode 100644
index 0000000..d558188
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/WidgetsBundleControllerTest.java
@@ -0,0 +1,316 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.page.TextPageData;
+import org.thingsboard.server.common.data.page.TextPageLink;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class WidgetsBundleControllerTest extends AbstractControllerTest {
+
+    private IdComparator<WidgetsBundle> idComparator = new IdComparator<>();
+
+    private Tenant savedTenant;
+    private User tenantAdmin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSaveWidgetsBundle() throws Exception {
+        WidgetsBundle widgetsBundle = new WidgetsBundle();
+        widgetsBundle.setTitle("My widgets bundle");
+        WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+
+        Assert.assertNotNull(savedWidgetsBundle);
+        Assert.assertNotNull(savedWidgetsBundle.getId());
+        Assert.assertNotNull(savedWidgetsBundle.getAlias());
+        Assert.assertTrue(savedWidgetsBundle.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedWidgetsBundle.getTenantId());
+        Assert.assertEquals(widgetsBundle.getTitle(), savedWidgetsBundle.getTitle());
+
+        savedWidgetsBundle.setTitle("My new widgets bundle");
+        doPost("/api/widgetsBundle", savedWidgetsBundle, WidgetsBundle.class);
+
+        WidgetsBundle foundWidgetsBundle = doGet("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString(), WidgetsBundle.class);
+        Assert.assertEquals(foundWidgetsBundle.getTitle(), savedWidgetsBundle.getTitle());
+    }
+
+    @Test
+    public void testFindWidgetsBundleById() throws Exception {
+        WidgetsBundle widgetsBundle = new WidgetsBundle();
+        widgetsBundle.setTitle("My widgets bundle");
+        WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+        WidgetsBundle foundWidgetsBundle = doGet("/api/widgetsBundle/" + savedWidgetsBundle.getId().getId().toString(), WidgetsBundle.class);
+        Assert.assertNotNull(foundWidgetsBundle);
+        Assert.assertEquals(savedWidgetsBundle, foundWidgetsBundle);
+    }
+
+    @Test
+    public void testDeleteWidgetsBundle() throws Exception {
+        WidgetsBundle widgetsBundle = new WidgetsBundle();
+        widgetsBundle.setTitle("My widgets bundle");
+        WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+
+        doDelete("/api/widgetsBundle/"+savedWidgetsBundle.getId().getId().toString())
+                .andExpect(status().isOk());
+
+        doGet("/api/widgetsBundle/"+savedWidgetsBundle.getId().getId().toString())
+                .andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testSaveWidgetsBundleWithEmptyTitle() throws Exception {
+        WidgetsBundle widgetsBundle = new WidgetsBundle();
+        doPost("/api/widgetsBundle", widgetsBundle)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Widgets bundle title should be specified")));
+    }
+
+    @Test
+    public void testUpdateWidgetsBundleAlias() throws Exception {
+        WidgetsBundle widgetsBundle = new WidgetsBundle();
+        widgetsBundle.setTitle("My widgets bundle");
+        WidgetsBundle savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+        savedWidgetsBundle.setAlias("new_alias");
+        doPost("/api/widgetsBundle", savedWidgetsBundle)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Update of widgets bundle alias is prohibited")));
+
+    }
+
+    @Test
+    public void testFindTenantWidgetsBundlesByPageLink() throws Exception {
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+
+        List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+
+        List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+        for (int i=0;i<73;i++) {
+            WidgetsBundle widgetsBundle = new WidgetsBundle();
+            widgetsBundle.setTitle("Widgets bundle"+i);
+            widgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+        }
+
+        widgetsBundles.addAll(sysWidgetsBundles);
+
+        List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(14);
+        TextPageData<WidgetsBundle> pageData;
+        do {
+            pageData = doGetTypedWithPageLink("/api/widgetsBundles?",
+                    new TypeReference<TextPageData<WidgetsBundle>>(){}, pageLink);
+            loadedWidgetsBundles.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(widgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+    }
+
+    @Test
+    public void testFindSystemWidgetsBundlesByPageLink() throws Exception {
+
+        loginSysAdmin();
+
+        List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        List<WidgetsBundle> createdWidgetsBundles = new ArrayList<>();
+        for (int i=0;i<120;i++) {
+            WidgetsBundle widgetsBundle = new WidgetsBundle();
+            widgetsBundle.setTitle("Widgets bundle"+i);
+            createdWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+        }
+
+        List<WidgetsBundle> widgetsBundles = new ArrayList<>(createdWidgetsBundles);
+        widgetsBundles.addAll(sysWidgetsBundles);
+
+        List<WidgetsBundle> loadedWidgetsBundles = new ArrayList<>();
+        TextPageLink pageLink = new TextPageLink(14);
+        TextPageData<WidgetsBundle> pageData;
+        do {
+            pageData = doGetTypedWithPageLink("/api/widgetsBundles?",
+                    new TypeReference<TextPageData<WidgetsBundle>>(){}, pageLink);
+            loadedWidgetsBundles.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(widgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+        for (WidgetsBundle widgetsBundle : createdWidgetsBundles) {
+            doDelete("/api/widgetsBundle/"+widgetsBundle.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        pageLink = new TextPageLink(17);
+        loadedWidgetsBundles.clear();
+        do {
+            pageData = doGetTypedWithPageLink("/api/widgetsBundles?",
+                    new TypeReference<TextPageData<WidgetsBundle>>(){}, pageLink);
+            loadedWidgetsBundles.addAll(pageData.getData());
+            if (pageData.hasNext()) {
+                pageLink = pageData.getNextPageLink();
+            }
+        } while (pageData.hasNext());
+
+        Collections.sort(sysWidgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(sysWidgetsBundles, loadedWidgetsBundles);
+    }
+
+
+    @Test
+    public void testFindTenantWidgetsBundles() throws Exception {
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+
+        List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+        for (int i=0;i<73;i++) {
+            WidgetsBundle widgetsBundle = new WidgetsBundle();
+            widgetsBundle.setTitle("Widgets bundle"+i);
+            widgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+        }
+
+        widgetsBundles.addAll(sysWidgetsBundles);
+
+        List<WidgetsBundle> loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        Collections.sort(widgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+    }
+
+    @Test
+    public void testFindSystemAndTenantWidgetsBundles() throws Exception {
+
+        loginSysAdmin();
+
+
+        List<WidgetsBundle> sysWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        List<WidgetsBundle> createdSystemWidgetsBundles = new ArrayList<>();
+        for (int i=0;i<82;i++) {
+            WidgetsBundle widgetsBundle = new WidgetsBundle();
+            widgetsBundle.setTitle("Sys widgets bundle"+i);
+            createdSystemWidgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+        }
+
+        List<WidgetsBundle> systemWidgetsBundles = new ArrayList<>(createdSystemWidgetsBundles);
+        systemWidgetsBundles.addAll(sysWidgetsBundles);
+
+        List<WidgetsBundle> widgetsBundles = new ArrayList<>();
+        widgetsBundles.addAll(systemWidgetsBundles);
+
+        login(tenantAdmin.getEmail(), "testPassword1");
+
+        for (int i=0;i<127;i++) {
+            WidgetsBundle widgetsBundle = new WidgetsBundle();
+            widgetsBundle.setTitle("Tenant widgets bundle"+i);
+            widgetsBundles.add(doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class));
+        }
+
+        List<WidgetsBundle> loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        Collections.sort(widgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(widgetsBundles, loadedWidgetsBundles);
+
+        loginSysAdmin();
+
+        loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        Collections.sort(systemWidgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(systemWidgetsBundles, loadedWidgetsBundles);
+
+        for (WidgetsBundle widgetsBundle : createdSystemWidgetsBundles) {
+            doDelete("/api/widgetsBundle/"+widgetsBundle.getId().getId().toString())
+                    .andExpect(status().isOk());
+        }
+
+        loadedWidgetsBundles = doGetTyped("/api/widgetsBundles?",
+                new TypeReference<List<WidgetsBundle>>(){});
+
+        Collections.sort(sysWidgetsBundles, idComparator);
+        Collections.sort(loadedWidgetsBundles, idComparator);
+
+        Assert.assertEquals(sysWidgetsBundles, loadedWidgetsBundles);
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java
new file mode 100644
index 0000000..3a65055
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/WidgetTypeControllerTest.java
@@ -0,0 +1,233 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.controller;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.widget.WidgetType;
+import org.thingsboard.server.common.data.widget.WidgetsBundle;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class WidgetTypeControllerTest extends AbstractControllerTest {
+
+    private IdComparator<WidgetType> idComparator = new IdComparator<>();
+
+    private Tenant savedTenant;
+    private WidgetsBundle savedWidgetsBundle;
+    private User tenantAdmin;
+
+    @Before
+    public void beforeTest() throws Exception {
+        loginSysAdmin();
+
+        Tenant tenant = new Tenant();
+        tenant.setTitle("My tenant");
+        savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+        Assert.assertNotNull(savedTenant);
+
+        tenantAdmin = new User();
+        tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
+        tenantAdmin.setTenantId(savedTenant.getId());
+        tenantAdmin.setEmail("tenant2@thingsboard.org");
+        tenantAdmin.setFirstName("Joe");
+        tenantAdmin.setLastName("Downs");
+
+        tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+        WidgetsBundle widgetsBundle = new WidgetsBundle();
+        widgetsBundle.setTitle("My widgets bundle");
+        savedWidgetsBundle = doPost("/api/widgetsBundle", widgetsBundle, WidgetsBundle.class);
+
+    }
+
+    @After
+    public void afterTest() throws Exception {
+        loginSysAdmin();
+
+        doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+                .andExpect(status().isOk());
+    }
+
+    @Test
+    public void testSaveWidgetType() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+
+        Assert.assertNotNull(savedWidgetType);
+        Assert.assertNotNull(savedWidgetType.getId());
+        Assert.assertNotNull(savedWidgetType.getAlias());
+        Assert.assertTrue(savedWidgetType.getCreatedTime() > 0);
+        Assert.assertEquals(savedTenant.getId(), savedWidgetType.getTenantId());
+        Assert.assertEquals(widgetType.getName(), savedWidgetType.getName());
+        Assert.assertEquals(widgetType.getDescriptor(), savedWidgetType.getDescriptor());
+        Assert.assertEquals(savedWidgetsBundle.getAlias(), savedWidgetType.getBundleAlias());
+
+        savedWidgetType.setName("New Widget Type");
+
+        doPost("/api/widgetType", savedWidgetType, WidgetType.class);
+
+        WidgetType foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetType.class);
+        Assert.assertEquals(foundWidgetType.getName(), savedWidgetType.getName());
+    }
+
+    @Test
+    public void testFindWidgetTypeById() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+        WidgetType foundWidgetType = doGet("/api/widgetType/" + savedWidgetType.getId().getId().toString(), WidgetType.class);
+        Assert.assertNotNull(foundWidgetType);
+        Assert.assertEquals(savedWidgetType, foundWidgetType);
+    }
+
+    @Test
+    public void testDeleteWidgetType() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+
+        doDelete("/api/widgetType/"+savedWidgetType.getId().getId().toString())
+                .andExpect(status().isOk());
+
+        doGet("/api/widgetType/"+savedWidgetType.getId().getId().toString())
+                .andExpect(status().isNotFound());
+    }
+
+    @Test
+    public void testSaveWidgetTypeWithEmptyName() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        doPost("/api/widgetType", widgetType)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Widgets type name should be specified")));
+    }
+
+    @Test
+    public void testSaveWidgetTypeWithEmptyBundleAlias() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        doPost("/api/widgetType", widgetType)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Widgets type bundle alias should be specified")));
+    }
+
+    @Test
+    public void testSaveWidgetTypeWithEmptyDescriptor() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{}", JsonNode.class));
+        doPost("/api/widgetType", widgetType)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Widgets type descriptor can't be empty")));
+    }
+
+    @Test
+    public void testSaveWidgetTypeWithInvalidBundleAlias() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias("some_alias");
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        doPost("/api/widgetType", widgetType)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Widget type is referencing to non-existent widgets bundle")));
+    }
+
+    @Test
+    public void testUpdateWidgetTypeBundleAlias() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+        savedWidgetType.setBundleAlias("some_alias");
+        doPost("/api/widgetType", savedWidgetType)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Update of widget type bundle alias is prohibited")));
+
+    }
+
+    @Test
+    public void testUpdateWidgetTypeAlias() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+        savedWidgetType.setAlias("some_alias");
+        doPost("/api/widgetType", savedWidgetType)
+                .andExpect(status().isBadRequest())
+                .andExpect(statusReason(containsString("Update of widget type alias is prohibited")));
+
+    }
+
+    @Test
+    public void testGetBundleWidgetTypes() throws Exception {
+        List<WidgetType> widgetTypes = new ArrayList<>();
+        for (int i=0;i<89;i++) {
+            WidgetType widgetType = new WidgetType();
+            widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+            widgetType.setName("Widget Type " + i);
+            widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+            widgetTypes.add(doPost("/api/widgetType", widgetType, WidgetType.class));
+        }
+
+        List<WidgetType> loadedWidgetTypes = doGetTyped("/api/widgetTypes?isSystem={isSystem}&bundleAlias={bundleAlias}",
+                new TypeReference<List<WidgetType>>(){}, false, savedWidgetsBundle.getAlias());
+
+        Collections.sort(widgetTypes, idComparator);
+        Collections.sort(loadedWidgetTypes, idComparator);
+
+        Assert.assertEquals(widgetTypes, loadedWidgetTypes);
+    }
+
+    @Test
+    public void testGetWidgetType() throws Exception {
+        WidgetType widgetType = new WidgetType();
+        widgetType.setBundleAlias(savedWidgetsBundle.getAlias());
+        widgetType.setName("Widget Type");
+        widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class));
+        WidgetType savedWidgetType = doPost("/api/widgetType", widgetType, WidgetType.class);
+        WidgetType foundWidgetType = doGet("/api/widgetType?isSystem={isSystem}&bundleAlias={bundleAlias}&alias={alias}",
+                WidgetType.class, false, savedWidgetsBundle.getAlias(), savedWidgetType.getAlias());
+        Assert.assertNotNull(foundWidgetType);
+        Assert.assertEquals(savedWidgetType, foundWidgetType);
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
new file mode 100644
index 0000000..6ba0207
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/service/mail/TestMailService.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.service.mail;
+
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+import org.springframework.context.annotation.Profile;
+import org.thingsboard.server.exception.ThingsboardException;
+
+@Profile("test")
+@Configuration
+public class TestMailService {
+
+    public static String currentActivateToken;
+    public static String currentResetPasswordToken;
+
+    @Bean
+    @Primary
+    public MailService mailService() throws ThingsboardException {
+        MailService mailService = Mockito.mock(MailService.class);
+        Mockito.doAnswer(new Answer<Void>() {
+            public Void answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                String activationLink = (String) args[0];
+                currentActivateToken = activationLink.split("=")[1];
+                return null;
+            }
+        }).when(mailService).sendActivationEmail(Mockito.anyString(), Mockito.anyString());
+        Mockito.doAnswer(new Answer<Void>() {
+            public Void answer(InvocationOnMock invocation) {
+                Object[] args = invocation.getArguments();
+                String passwordResetLink = (String) args[0];
+                currentResetPasswordToken = passwordResetLink.split("=")[1];
+                return null;
+            }
+        }).when(mailService).sendResetPasswordEmail(Mockito.anyString(), Mockito.anyString());
+        return mailService;
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
new file mode 100644
index 0000000..10d41be
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/system/HttpDeviceApiTest.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.system;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.test.web.servlet.ResultActions;
+import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.controller.AbstractControllerTest;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class HttpDeviceApiTest extends AbstractControllerTest {
+
+    private static final AtomicInteger idSeq = new AtomicInteger(new Random(System.currentTimeMillis()).nextInt());
+
+    protected Device device;
+    protected DeviceCredentials deviceCredentials;
+
+    @Before
+    public void before() throws Exception {
+        loginTenantAdmin();
+        device = new Device();
+        device.setName("My device");
+        device = doPost("/api/device", device, Device.class);
+
+        deviceCredentials =
+                doGet("/api/device/" + device.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+    }
+
+    @Test
+    public void testGetAttributes() throws Exception {
+        doGetAsync("/api/v1/" + "WRONG_TOKEN" + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isUnauthorized());
+        doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isNotFound());
+
+        Map<String, String> attrMap = new HashMap<>();
+        attrMap.put("keyA", "valueA");
+        mockMvc.perform(
+                asyncDispatch(doPost("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes", attrMap, new String[]{}).andReturn()))
+                .andExpect(status().isOk());
+        doGetAsync("/api/v1/" + deviceCredentials.getCredentialsId() + "/attributes?clientKeys=keyA,keyB,keyC").andExpect(status().isOk());
+    }
+
+    protected ResultActions doGetAsync(String urlTemplate, Object... urlVariables) throws Exception {
+        MockHttpServletRequestBuilder getRequest;
+        getRequest = get(urlTemplate, urlVariables);
+        setJwtToken(getRequest);
+        return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
+    }
+
+    protected ResultActions doPostAsync(String urlTemplate, Object... urlVariables) throws Exception {
+        MockHttpServletRequestBuilder getRequest = post(urlTemplate, urlVariables);
+        setJwtToken(getRequest);
+        return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn()));
+    }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/system/SystemTestSuite.java b/application/src/test/java/org/thingsboard/server/system/SystemTestSuite.java
new file mode 100644
index 0000000..9d565f7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/system/SystemTestSuite.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.system;
+
+import org.cassandraunit.dataset.cql.ClassPathCQLDataSet;
+import org.junit.ClassRule;
+import org.junit.extensions.cpsuite.ClasspathSuite;
+import org.junit.runner.RunWith;
+import org.thingsboard.server.dao.CustomCassandraCQLUnit;
+
+import java.util.Arrays;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RunWith(ClasspathSuite.class)
+@ClasspathSuite.ClassnameFilters({"org.thingsboard.server.system.*Test"})
+public class SystemTestSuite {
+
+    @ClassRule
+    public static CustomCassandraCQLUnit cassandraUnit =
+            new CustomCassandraCQLUnit(Arrays.asList(
+                    new ClassPathCQLDataSet("schema.cql", false, false),
+                    new ClassPathCQLDataSet("system-data.cql", false, false)),
+                    "cassandra-test.yaml", 30000l);
+}
diff --git a/application/src/test/java/org/thingsboard/server/ThingsboardApplicationTests.java b/application/src/test/java/org/thingsboard/server/ThingsboardApplicationTests.java
new file mode 100644
index 0000000..63b62d2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/ThingsboardApplicationTests.java
@@ -0,0 +1,49 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.boot.test.IntegrationTest;
+import org.springframework.boot.test.SpringApplicationConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringApplicationConfiguration(classes = ThingsboardServerApplication.class)
+@WebAppConfiguration
+@IntegrationTest("server.port:0")
+public class ThingsboardApplicationTests {
+
+    @Test
+    public void contextLoads() {
+        String test = "[   \n" +
+                "    {\n" +
+                "        \"key\": \"name\",\n" +
+                "\t\"type\": \"text\"        \n" +
+                "    },\n" +
+                "    {\n" +
+                "\t\"key\": \"name2\",\n" +
+                "\t\"type\": \"color\"\n" +
+                "    },\n" +
+                "    {\n" +
+                "\t\"key\": \"name3\",\n" +
+                "\t\"type\": \"javascript\"\n" +
+                "    }    \n" +
+                "]";
+    }
+
+}
diff --git a/application/src/test/resources/logback.xml b/application/src/test/resources/logback.xml
new file mode 100644
index 0000000..f32acec
--- /dev/null
+++ b/application/src/test/resources/logback.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+
+<configuration>
+    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+        <encoder>
+            <pattern>%d{ISO8601} [%thread] %-5level %logger{36} - %msg%n</pattern>
+        </encoder>
+    </appender>
+
+    <logger name="org.thingsboard.server" level="DEBUG"/>
+    <logger name="org.springframework" level="WARN"/>
+    <logger name="org.apache.cassandra" level="WARN"/>
+    <logger name="org.cassandraunit" level="INFO"/>
+
+    <logger name="akka" level="DEBUG" />
+
+    <root level="WARN">
+        <appender-ref ref="console"/>
+    </root>
+
+</configuration>

common/data/pom.xml 76(+76 -0)

diff --git a/common/data/pom.xml b/common/data/pom.xml
new file mode 100644
index 0000000..b458ef1
--- /dev/null
+++ b/common/data/pom.xml
@@ -0,0 +1,76 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard.server</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>common</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server.common</groupId>
+    <artifactId>data</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Common Data</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+		</dependency>        
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+        </plugins>
+    </build>
+
+</project>
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java
new file mode 100644
index 0000000..c91e4ee
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/AdminSettings.java
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.AdminSettingsId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class AdminSettings extends BaseData<AdminSettingsId> {
+
+    private static final long serialVersionUID = -7670322981725511892L;
+    
+    private String key;
+    private JsonNode jsonValue;
+    
+    public AdminSettings() {
+        super();
+    }
+
+    public AdminSettings(AdminSettingsId id) {
+        super(id);
+    }
+    
+    public AdminSettings(AdminSettings adminSettings) {
+        super(adminSettings);
+        this.key = adminSettings.getKey();
+        this.jsonValue = adminSettings.getJsonValue();
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public JsonNode getJsonValue() {
+        return jsonValue;
+    }
+
+    public void setJsonValue(JsonNode jsonValue) {
+        this.jsonValue = jsonValue;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((jsonValue == null) ? 0 : jsonValue.hashCode());
+        result = prime * result + ((key == null) ? 0 : key.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        AdminSettings other = (AdminSettings) obj;
+        if (jsonValue == null) {
+            if (other.jsonValue != null)
+                return false;
+        } else if (!jsonValue.equals(other.jsonValue))
+            return false;
+        if (key == null) {
+            if (other.key != null)
+                return false;
+        } else if (!key.equals(other.key))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("AdminSettings [key=");
+        builder.append(key);
+        builder.append(", jsonValue=");
+        builder.append(jsonValue);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java
new file mode 100644
index 0000000..452efa0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/BaseData.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import java.io.Serializable;
+
+import org.thingsboard.server.common.data.id.IdBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+public abstract class BaseData<I extends UUIDBased> extends IdBased<I> implements Serializable {
+
+    private static final long serialVersionUID = 5422817607129962637L;
+    
+    protected long createdTime;
+    
+    public BaseData() {
+        super();
+    }
+
+    public BaseData(I id) {
+        super(id);
+    }
+    
+    public BaseData(BaseData<I> data) {
+        super(data.getId());
+        this.createdTime = data.getCreatedTime();
+    }
+
+    public long getCreatedTime() {
+        return createdTime;
+    }
+
+    public void setCreatedTime(long createdTime) {
+        this.createdTime = createdTime;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + (int) (createdTime ^ (createdTime >>> 32));
+        return result;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        BaseData other = (BaseData) obj;
+        if (createdTime != other.createdTime)
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("BaseData [createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
new file mode 100644
index 0000000..ae62f41
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+public class CacheConstants {
+    public static final String DEVICE_CREDENTIALS_CACHE = "deviceCredentials";
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java
new file mode 100644
index 0000000..397435e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+public abstract class ContactBased<I extends UUIDBased> extends SearchTextBased<I> {
+    
+    private static final long serialVersionUID = 5047448057830660988L;
+    
+    protected String country;
+    protected String state;
+    protected String city;
+    protected String address;
+    protected String address2;
+    protected String zip;
+    protected String phone;
+    protected String email;
+    
+    public ContactBased() {
+        super();
+    }
+
+    public ContactBased(I id) {
+        super(id);
+    }
+    
+    public ContactBased(ContactBased<I> contact) {
+        super(contact);
+        this.country = contact.getCountry();
+        this.state = contact.getState();
+        this.city = contact.getCity();
+        this.address = contact.getAddress();
+        this.address2 = contact.getAddress2();
+        this.zip = contact.getZip();
+        this.phone = contact.getPhone();
+        this.email = contact.getEmail();
+    }
+
+    public String getCountry() {
+        return country;
+    }
+
+    public void setCountry(String country) {
+        this.country = country;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    public String getCity() {
+        return city;
+    }
+
+    public void setCity(String city) {
+        this.city = city;
+    }
+
+    public String getAddress() {
+        return address;
+    }
+
+    public void setAddress(String address) {
+        this.address = address;
+    }
+
+    public String getAddress2() {
+        return address2;
+    }
+
+    public void setAddress2(String address2) {
+        this.address2 = address2;
+    }
+
+    public String getZip() {
+        return zip;
+    }
+
+    public void setZip(String zip) {
+        this.zip = zip;
+    }
+
+    public String getPhone() {
+        return phone;
+    }
+
+    public void setPhone(String phone) {
+        this.phone = phone;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((address == null) ? 0 : address.hashCode());
+        result = prime * result + ((address2 == null) ? 0 : address2.hashCode());
+        result = prime * result + ((city == null) ? 0 : city.hashCode());
+        result = prime * result + ((country == null) ? 0 : country.hashCode());
+        result = prime * result + ((email == null) ? 0 : email.hashCode());
+        result = prime * result + ((phone == null) ? 0 : phone.hashCode());
+        result = prime * result + ((state == null) ? 0 : state.hashCode());
+        result = prime * result + ((zip == null) ? 0 : zip.hashCode());
+        return result;
+    }
+
+    @SuppressWarnings("rawtypes")
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        ContactBased other = (ContactBased) obj;
+        if (address == null) {
+            if (other.address != null)
+                return false;
+        } else if (!address.equals(other.address))
+            return false;
+        if (address2 == null) {
+            if (other.address2 != null)
+                return false;
+        } else if (!address2.equals(other.address2))
+            return false;
+        if (city == null) {
+            if (other.city != null)
+                return false;
+        } else if (!city.equals(other.city))
+            return false;
+        if (country == null) {
+            if (other.country != null)
+                return false;
+        } else if (!country.equals(other.country))
+            return false;
+        if (email == null) {
+            if (other.email != null)
+                return false;
+        } else if (!email.equals(other.email))
+            return false;
+        if (phone == null) {
+            if (other.phone != null)
+                return false;
+        } else if (!phone.equals(other.phone))
+            return false;
+        if (state == null) {
+            if (other.state != null)
+                return false;
+        } else if (!state.equals(other.state))
+            return false;
+        if (zip == null) {
+            if (other.zip != null)
+                return false;
+        } else if (!zip.equals(other.zip))
+            return false;
+        return true;
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
new file mode 100644
index 0000000..3d06bdc
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java
@@ -0,0 +1,145 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Customer extends ContactBased<CustomerId>{
+    
+    private static final long serialVersionUID = -1599722990298929275L;
+    
+    private String title;
+    private TenantId tenantId;
+    private JsonNode additionalInfo;
+    
+    public Customer() {
+        super();
+    }
+
+    public Customer(CustomerId id) {
+        super(id);
+    }
+    
+    public Customer(Customer customer) {
+        super(customer);
+        this.tenantId = customer.getTenantId();
+        this.title = customer.getTitle();
+        this.additionalInfo = customer.getAdditionalInfo();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+    
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+    
+    @Override
+    public String getSearchText() {
+        return title;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        result = prime * result + ((title == null) ? 0 : title.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Customer other = (Customer) obj;
+        if (additionalInfo == null) {
+            if (other.additionalInfo != null)
+                return false;
+        } else if (!additionalInfo.equals(other.additionalInfo))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        if (title == null) {
+            if (other.title != null)
+                return false;
+        } else if (!title.equals(other.title))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Customer [title=");
+        builder.append(title);
+        builder.append(", tenantId=");
+        builder.append(tenantId);
+        builder.append(", additionalInfo=");
+        builder.append(additionalInfo);
+        builder.append(", country=");
+        builder.append(country);
+        builder.append(", state=");
+        builder.append(state);
+        builder.append(", city=");
+        builder.append(city);
+        builder.append(", address=");
+        builder.append(address);
+        builder.append(", address2=");
+        builder.append(address2);
+        builder.append(", zip=");
+        builder.append(zip);
+        builder.append(", phone=");
+        builder.append(phone);
+        builder.append(", email=");
+        builder.append(email);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
new file mode 100644
index 0000000..72879c9
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Dashboard.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DashboardId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Dashboard extends SearchTextBased<DashboardId> {
+
+    private static final long serialVersionUID = 872682138346187503L;
+    
+    private TenantId tenantId;
+    private CustomerId customerId;
+    private String title;
+    private JsonNode configuration;
+    
+    public Dashboard() {
+        super();
+    }
+
+    public Dashboard(DashboardId id) {
+        super(id);
+    }
+    
+    public Dashboard(Dashboard dashboard) {
+        super(dashboard);
+        this.tenantId = dashboard.getTenantId();
+        this.customerId = dashboard.getCustomerId();
+        this.title = dashboard.getTitle();
+        this.configuration = dashboard.getConfiguration();
+    }
+    
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    public void setCustomerId(CustomerId customerId) {
+        this.customerId = customerId;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public JsonNode getConfiguration() {
+        return configuration;
+    }
+
+    public void setConfiguration(JsonNode configuration) {
+        this.configuration = configuration;
+    }
+
+    @Override
+    public String getSearchText() {
+        return title;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((configuration == null) ? 0 : configuration.hashCode());
+        result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        result = prime * result + ((title == null) ? 0 : title.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Dashboard other = (Dashboard) obj;
+        if (configuration == null) {
+            if (other.configuration != null)
+                return false;
+        } else if (!configuration.equals(other.configuration))
+            return false;
+        if (customerId == null) {
+            if (other.customerId != null)
+                return false;
+        } else if (!customerId.equals(other.customerId))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        if (title == null) {
+            if (other.title != null)
+                return false;
+        } else if (!title.equals(other.title))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Dashboard [tenantId=");
+        builder.append(tenantId);
+        builder.append(", customerId=");
+        builder.append(customerId);
+        builder.append(", title=");
+        builder.append(title);
+        builder.append(", configuration=");
+        builder.append(configuration);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
new file mode 100644
index 0000000..34ae9b2
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DataConstants {
+
+    public static final String SYSTEM = "SYSTEM";
+    public static final String TENANT = "TENANT";
+    public static final String CUSTOMER = "CUSTOMER";
+    public static final String DEVICE = "DEVICE";
+
+    public static final String CLIENT_SCOPE = "CLIENT_SCOPE";
+    public static final String SERVER_SCOPE = "SERVER_SCOPE";
+    public static final String SHARED_SCOPE = "SHARED_SCOPE";
+
+    public static final String ALARM = "ALARM";
+    public static final String ERROR = "ERROR";
+    public static final String LC_EVENT = "LC_EVENT";
+    public static final String STATS = "STATS";
+
+    public static final String ONEWAY = "ONEWAY";
+    public static final String TWOWAY = "TWOWAY";
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
new file mode 100644
index 0000000..aece691
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java
@@ -0,0 +1,148 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Device extends SearchTextBased<DeviceId> {
+
+    private static final long serialVersionUID = 2807343040519543363L;
+
+    private TenantId tenantId;
+    private CustomerId customerId;
+    private String name;
+    private JsonNode additionalInfo;
+
+    public Device() {
+        super();
+    }
+
+    public Device(DeviceId id) {
+        super(id);
+    }
+
+    public Device(Device device) {
+        super(device);
+        this.tenantId = device.getTenantId();
+        this.customerId = device.getCustomerId();
+        this.name = device.getName();
+        this.additionalInfo = device.getAdditionalInfo();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    public void setCustomerId(CustomerId customerId) {
+        this.customerId = customerId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+    
+    @Override
+    public String getSearchText() {
+        return name;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+        result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Device other = (Device) obj;
+        if (additionalInfo == null) {
+            if (other.additionalInfo != null)
+                return false;
+        } else if (!additionalInfo.equals(other.additionalInfo))
+            return false;
+        if (customerId == null) {
+            if (other.customerId != null)
+                return false;
+        } else if (!customerId.equals(other.customerId))
+            return false;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Device [tenantId=");
+        builder.append(tenantId);
+        builder.append(", customerId=");
+        builder.append(customerId);
+        builder.append(", name=");
+        builder.append(name);
+        builder.append(", additionalInfo=");
+        builder.append(additionalInfo);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
new file mode 100644
index 0000000..b1247ea
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum EntityType {
+    TENANT, DEVICE, CUSTOMER, RULE, PLUGIN
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Event.java b/common/data/src/main/java/org/thingsboard/server/common/data/Event.java
new file mode 100644
index 0000000..1298907
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Event.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.EventId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class Event extends BaseData<EventId> {
+
+    private TenantId tenantId;
+    private String type;
+    private String uid;
+    private EntityId entityId;
+    private JsonNode body;
+
+    public Event() {
+        super();
+    }
+
+    public Event(EventId id) {
+        super(id);
+    }
+
+    public Event(Event event) {
+        super(event);
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
new file mode 100644
index 0000000..2f31136
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/AdminSettingsId.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class AdminSettingsId extends UUIDBased {
+
+    @JsonCreator
+    public AdminSettingsId(@JsonProperty("id") UUID id){
+        super(id);
+    }
+    
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java
new file mode 100644
index 0000000..43b023b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/ComponentDescriptorId.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.UUID;
+
+public final class ComponentDescriptorId extends UUIDBased {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public ComponentDescriptorId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
new file mode 100644
index 0000000..83fc95f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/CustomerId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+public final class CustomerId extends UUIDBased implements EntityId {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public CustomerId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.CUSTOMER;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
new file mode 100644
index 0000000..16968a4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DashboardId.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DashboardId extends UUIDBased {
+
+    @JsonCreator
+    public DashboardId(@JsonProperty("id") UUID id){
+        super(id);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceCredentialsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceCredentialsId.java
new file mode 100644
index 0000000..58b86d0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceCredentialsId.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeviceCredentialsId extends UUIDBased {
+
+    @JsonCreator
+    public DeviceCredentialsId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
new file mode 100644
index 0000000..04d6790
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/DeviceId.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+public class DeviceId extends UUIDBased implements EntityId {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public DeviceId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    public static DeviceId fromString(String deviceId) {
+        return new DeviceId(UUID.fromString(deviceId));
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.DEVICE;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
new file mode 100644
index 0000000..30612a1
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface EntityId {
+
+    UUID NULL_UUID = UUID.fromString("13814000-1dd2-11b2-8080-808080808080");
+
+    UUID getId();
+
+    EntityType getEntityType();
+
+    @JsonIgnore
+    default boolean isNullUid() {
+        return NULL_UUID.equals(getId());
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EventId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EventId.java
new file mode 100644
index 0000000..12fd9d1
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EventId.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.UUID;
+
+public class EventId extends UUIDBased {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public EventId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    public static EventId fromString(String eventId) {
+        return new EventId(UUID.fromString(eventId));
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
new file mode 100644
index 0000000..1f98db8
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/IdBased.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+import java.util.UUID;
+
+public abstract class IdBased<I extends UUIDBased> {
+	
+	protected I id;
+	
+	public IdBased() {
+		super();
+	}
+	
+	public IdBased(I id) {
+		super();
+		this.id = id;
+	}
+
+	public void setId(I id) {
+		this.id = id;
+	}
+
+	public I getId() {
+		return id;
+	}
+
+	@JsonIgnore
+	public UUID getUuidId() {
+		if (id != null) {
+			return id.getId();
+		}
+		return null;
+	}
+
+	@Override
+	public int hashCode() {
+		final int prime = 31;
+		int result = 1;
+		result = prime * result + ((id == null) ? 0 : id.hashCode());
+		return result;
+	}
+
+	@SuppressWarnings("rawtypes")
+	@Override
+	public boolean equals(Object obj) {
+		if (this == obj)
+			return true;
+		if (obj == null)
+			return false;
+		if (getClass() != obj.getClass())
+			return false;
+		IdBased other = (IdBased) obj;
+		if (id == null) {
+			if (other.id != null)
+				return false;
+		} else if (!id.equals(other.id))
+			return false;
+		return true;
+	}
+	
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NodeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NodeId.java
new file mode 100644
index 0000000..e77302d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NodeId.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+public class NodeId extends UUIDBased {
+
+	public NodeId(UUID id){
+		super(id);
+	}
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/PluginId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/PluginId.java
new file mode 100644
index 0000000..f86e8dd
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/PluginId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+public final class PluginId extends UUIDBased implements EntityId {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public PluginId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.PLUGIN;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleId.java
new file mode 100644
index 0000000..05d822f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/RuleId.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+import java.util.UUID;
+
+public class RuleId extends UUIDBased implements EntityId {
+
+    @JsonCreator
+    public RuleId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.RULE;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/SessionId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/SessionId.java
new file mode 100644
index 0000000..dc6f883
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/SessionId.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.io.Serializable;
+
+public interface SessionId extends Serializable {
+
+    String toUidStr();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
new file mode 100644
index 0000000..8ef157b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/TenantId.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.EntityType;
+
+public final class TenantId extends UUIDBased implements EntityId {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public TenantId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+    @JsonIgnore
+    @Override
+    public EntityType getEntityType() {
+        return EntityType.TENANT;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java
new file mode 100644
index 0000000..d172793
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserCredentialsId.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+public class UserCredentialsId extends UUIDBased {
+
+    public UserCredentialsId(UUID id){
+        super(id);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
new file mode 100644
index 0000000..53531d7
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UserId.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class UserId extends UUIDBased {
+
+    @JsonCreator
+	public UserId(@JsonProperty("id") UUID id){
+		super(id);
+	}
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java
new file mode 100644
index 0000000..b8d07e0
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/UUIDBased.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public abstract class UUIDBased implements Serializable {
+
+    public static final UUID EMPTY = new UUID(0L, 0L);
+
+    private static final long serialVersionUID = 1L;
+
+    private final UUID id;
+
+    public UUIDBased() {
+        this(UUID.randomUUID());
+    }
+
+    public UUIDBased(UUID id) {
+        super();
+        this.id = id;
+    }
+
+    public UUID getId() {
+        return id;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((id == null) ? 0 : id.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        UUIDBased other = (UUIDBased) obj;
+        if (id == null) {
+            if (other.id != null)
+                return false;
+        } else if (!id.equals(other.id))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return id.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
new file mode 100644
index 0000000..2e97310
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetsBundleId.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class WidgetsBundleId extends UUIDBased {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public WidgetsBundleId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
new file mode 100644
index 0000000..39d6518
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/WidgetTypeId.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.id;
+
+import java.util.UUID;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public final class WidgetTypeId extends UUIDBased {
+
+    private static final long serialVersionUID = 1L;
+
+    @JsonCreator
+    public WidgetTypeId(@JsonProperty("id") UUID id) {
+        super(id);
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKey.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKey.java
new file mode 100644
index 0000000..ae23773
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKey.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class AttributeKey implements Serializable {
+    private final String scope;
+    private final String attributeKey;
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java
new file mode 100644
index 0000000..42af854
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/AttributeKvEntry.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface AttributeKvEntry extends KvEntry {
+
+    long getLastUpdateTs();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java
new file mode 100644
index 0000000..afe430c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseAttributeKvEntry.java
@@ -0,0 +1,104 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class BaseAttributeKvEntry implements AttributeKvEntry {
+
+    private final long lastUpdateTs;
+    private final KvEntry kv;
+
+    public BaseAttributeKvEntry(KvEntry kv, long lastUpdateTs) {
+        this.kv = kv;
+        this.lastUpdateTs = lastUpdateTs;
+    }
+
+    @Override
+    public long getLastUpdateTs() {
+        return lastUpdateTs;
+    }
+
+    @Override
+    public String getKey() {
+        return kv.getKey();
+    }
+
+    @Override
+    public DataType getDataType() {
+        return kv.getDataType();
+    }
+
+    @Override
+    public Optional<String> getStrValue() {
+        return kv.getStrValue();
+    }
+
+    @Override
+    public Optional<Long> getLongValue() {
+        return kv.getLongValue();
+    }
+
+    @Override
+    public Optional<Boolean> getBooleanValue() {
+        return kv.getBooleanValue();
+    }
+
+    @Override
+    public Optional<Double> getDoubleValue() {
+        return kv.getDoubleValue();
+    }
+
+    @Override
+    public String getValueAsString() {
+        return kv.getValueAsString();
+    }
+
+    @Override
+    public Object getValue() {
+        return kv.getValue();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        BaseAttributeKvEntry that = (BaseAttributeKvEntry) o;
+
+        if (lastUpdateTs != that.lastUpdateTs) return false;
+        return kv.equals(that.kv);
+
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (int) (lastUpdateTs ^ (lastUpdateTs >>> 32));
+        result = 31 * result + kv.hashCode();
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "BaseAttributeKvEntry{" +
+                "lastUpdateTs=" + lastUpdateTs +
+                ", kv=" + kv +
+                '}';
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
new file mode 100644
index 0000000..e74a91e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BaseTsKvQuery.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Optional;
+
+public class BaseTsKvQuery implements TsKvQuery {
+
+    private String key;
+    private Optional<Long> startTs;
+    private Optional<Long> endTs;
+    private Optional<Integer> limit;
+
+    public BaseTsKvQuery(String key, Optional<Long> startTs, Optional<Long> endTs, Optional<Integer> limit) {
+        this.key = key;
+        this.startTs = startTs;
+        this.endTs = endTs;
+        this.limit = limit;
+    }
+    
+    public BaseTsKvQuery(String key, Long startTs, Long endTs, Integer limit) {
+        this(key, Optional.ofNullable(startTs), Optional.ofNullable(endTs), Optional.ofNullable(limit));
+    }
+
+    public BaseTsKvQuery(String key, Long startTs, Integer limit) {
+        this(key, startTs, null, limit);
+    }
+
+    public BaseTsKvQuery(String key, Long startTs, Long endTs) {
+        this(key, startTs, endTs, null);
+    }
+
+    public BaseTsKvQuery(String key, Long startTs) {
+        this(key, startTs, null, null);
+    }
+
+    public BaseTsKvQuery(String key, Integer limit) {
+        this(key, null, null, limit);
+    }
+
+    @Override
+    public String getKey() {
+        return key;
+    }
+
+    @Override
+    public Optional<Long> getStartTs() {
+        return startTs;
+    }
+
+    @Override
+    public Optional<Long> getEndTs() {
+        return endTs;
+    }
+
+    @Override
+    public Optional<Integer> getLimit() {
+        return limit;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java
new file mode 100644
index 0000000..e0e0a4b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicKvEntry.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public abstract class BasicKvEntry implements KvEntry {
+
+    private final String key;
+
+    protected BasicKvEntry(String key) {
+        this.key = key;
+    }
+
+    @Override
+    public String getKey() {
+        return key;
+    }
+
+    @Override
+    public Optional<String> getStrValue() {
+        return Optional.ofNullable(null);
+    }
+
+    @Override
+    public Optional<Long> getLongValue() {
+        return Optional.ofNullable(null);
+    }
+
+    @Override
+    public Optional<Boolean> getBooleanValue() {
+        return Optional.ofNullable(null);
+    }
+
+    @Override
+    public Optional<Double> getDoubleValue() {
+        return Optional.ofNullable(null);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof BasicKvEntry)) return false;
+        BasicKvEntry that = (BasicKvEntry) o;
+        return Objects.equals(key, that.key);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(key);
+    }
+
+    @Override
+    public String toString() {
+        return "BasicKvEntry{" +
+                "key='" + key + '\'' +
+                '}';
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
new file mode 100644
index 0000000..b4442fa
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BasicTsKvEntry.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class BasicTsKvEntry implements TsKvEntry {
+
+    private final long ts;
+    private final KvEntry kv;
+
+    public BasicTsKvEntry(long ts, KvEntry kv) {
+        this.ts = ts;
+        this.kv = kv;
+    }
+
+    @Override
+    public String getKey() {
+        return kv.getKey();
+    }
+
+    @Override
+    public DataType getDataType() {
+        return kv.getDataType();
+    }
+
+    @Override
+    public Optional<String> getStrValue() {
+        return kv.getStrValue();
+    }
+
+    @Override
+    public Optional<Long> getLongValue() {
+        return kv.getLongValue();
+    }
+
+    @Override
+    public Optional<Boolean> getBooleanValue() {
+        return kv.getBooleanValue();
+    }
+
+    @Override
+    public Optional<Double> getDoubleValue() {
+        return kv.getDoubleValue();
+    }
+
+    @Override
+    public Object getValue() {
+        return kv.getValue();
+    }
+
+    @Override
+    public long getTs() {
+        return ts;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof BasicTsKvEntry)) return false;
+        BasicTsKvEntry that = (BasicTsKvEntry) o;
+        return getTs() == that.getTs() &&
+                Objects.equals(kv, that.kv);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(getTs(), kv);
+    }
+
+    @Override
+    public String toString() {
+        return "BasicTsKvEntry{" +
+                "ts=" + ts +
+                ", kv=" + kv +
+                '}';
+    }
+
+    @Override
+    public String getValueAsString() {
+        return kv.getValueAsString();
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/BooleanDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BooleanDataEntry.java
new file mode 100644
index 0000000..f13c723
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/BooleanDataEntry.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class BooleanDataEntry extends BasicKvEntry {
+    private final Boolean value;
+
+    public BooleanDataEntry(String key, Boolean value) {
+        super(key);
+        this.value = value;
+    }
+
+    @Override
+    public DataType getDataType() {
+        return DataType.BOOLEAN;
+    }
+
+    @Override
+    public Optional<Boolean> getBooleanValue() {
+        return Optional.of(value);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof BooleanDataEntry)) return false;
+        if (!super.equals(o)) return false;
+        BooleanDataEntry that = (BooleanDataEntry) o;
+        return Objects.equals(value, that.value);
+    }
+
+    @Override
+    public Object getValue() {
+        return value;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), value);
+    }
+
+    @Override
+    public String toString() {
+        return "BooleanDataEntry{" +
+                "value=" + value +
+                "} " + super.toString();
+    }
+
+    @Override
+    public String getValueAsString() {
+        return Boolean.toString(value);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java
new file mode 100644
index 0000000..68f5358
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DataType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+public enum DataType {
+
+    STRING, LONG, BOOLEAN, DOUBLE;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/DoubleDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DoubleDataEntry.java
new file mode 100644
index 0000000..e9cfe1c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/DoubleDataEntry.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class DoubleDataEntry extends BasicKvEntry {
+
+    private final Double value;
+
+    public DoubleDataEntry(String key, Double value) {
+        super(key);
+        this.value = value;
+    }
+
+    @Override
+    public DataType getDataType() {
+        return DataType.DOUBLE;
+    }
+
+    @Override
+    public Optional<Double> getDoubleValue() {
+        return Optional.of(value);
+    }
+
+    @Override
+    public Object getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof DoubleDataEntry)) return false;
+        if (!super.equals(o)) return false;
+        DoubleDataEntry that = (DoubleDataEntry) o;
+        return Objects.equals(value, that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), value);
+    }
+
+    @Override
+    public String toString() {
+        return "DoubleDataEntry{" +
+                "value=" + value +
+                "} " + super.toString();
+    }
+    
+    @Override
+    public String getValueAsString() {
+        return Double.toString(value);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/KvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/KvEntry.java
new file mode 100644
index 0000000..eeaa70c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/KvEntry.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+/**
+ * Represents attribute or any other KV data entry
+ *
+ * @author ashvayka
+ */
+public interface KvEntry extends Serializable {
+
+    String getKey();
+
+    DataType getDataType();
+
+    Optional<String> getStrValue();
+
+    Optional<Long> getLongValue();
+
+    Optional<Boolean> getBooleanValue();
+
+    Optional<Double> getDoubleValue();
+
+    String getValueAsString();
+
+    Object getValue();
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/LongDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/LongDataEntry.java
new file mode 100644
index 0000000..b72ee00
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/LongDataEntry.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class LongDataEntry extends BasicKvEntry {
+
+    private final Long value;
+
+    public LongDataEntry(String key, Long value) {
+        super(key);
+        this.value = value;
+    }
+
+    @Override
+    public DataType getDataType() {
+        return DataType.LONG;
+    }
+
+    @Override
+    public Optional<Long> getLongValue() {
+        return Optional.of(value);
+    }
+
+    @Override
+    public Object getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof LongDataEntry)) return false;
+        if (!super.equals(o)) return false;
+        LongDataEntry that = (LongDataEntry) o;
+        return Objects.equals(value, that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), value);
+    }
+
+    @Override
+    public String toString() {
+        return "LongDataEntry{" +
+                "value=" + value +
+                "} " + super.toString();
+    }
+    
+    @Override
+    public String getValueAsString() {
+        return Long.toString(value);
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java
new file mode 100644
index 0000000..2512d70
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Objects;
+import java.util.Optional;
+
+public class StringDataEntry extends BasicKvEntry {
+
+    private static final long serialVersionUID = 1L;
+    private final String value;
+
+    public StringDataEntry(String key, String value) {
+        super(key);
+        this.value = value;
+    }
+
+    @Override
+    public DataType getDataType() {
+        return DataType.STRING;
+    }
+
+    @Override
+    public Optional<String> getStrValue() {
+        return Optional.of(value);
+    }
+
+    @Override
+    public Object getValue() {
+        return value;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o)
+            return true;
+        if (!(o instanceof StringDataEntry))
+            return false;
+        if (!super.equals(o))
+            return false;
+        StringDataEntry that = (StringDataEntry) o;
+        return Objects.equals(value, that.value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), value);
+    }
+
+    @Override
+    public String toString() {
+        return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString();
+    }
+    
+    @Override
+    public String getValueAsString() {
+        return value;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
new file mode 100644
index 0000000..cf5e275
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvEntry.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+/**
+ * Represents time series KV data entry
+ * 
+ * @author ashvayka
+ *
+ */
+public interface TsKvEntry extends KvEntry {
+
+    long getTs();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
new file mode 100644
index 0000000..f1e89e6
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/kv/TsKvQuery.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.kv;
+
+import java.util.Optional;
+
+public interface TsKvQuery {
+
+    String getKey();
+
+    Optional<Long> getStartTs();
+
+    Optional<Long> getEndTs();
+
+    Optional<Integer> getLimit();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java
new file mode 100644
index 0000000..ae19f39
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/BasePageLink.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.page;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@AllArgsConstructor
+public abstract class BasePageLink implements Serializable {
+
+    private static final long serialVersionUID = -4189954843653250481L;
+
+    @Getter protected final int limit;
+
+    @Getter @Setter protected UUID idOffset;
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
new file mode 100644
index 0000000..692d472
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/PageDataIterable.java
@@ -0,0 +1,78 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.page;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+public class PageDataIterable<T extends SearchTextBased<? extends UUIDBased>> implements Iterable<T>, Iterator<T> {
+
+    private final FetchFunction<T> function;
+    private final int fetchSize;
+
+    private List<T> currentItems;
+    private int currentIdx;
+    private boolean hasNextPack;
+    private TextPageLink nextPackLink;
+    private boolean initialized;
+
+    public PageDataIterable(FetchFunction<T> function, int fetchSize) {
+        super();
+        this.function = function;
+        this.fetchSize = fetchSize;
+    }
+
+    @Override
+    public Iterator<T> iterator() {
+        return this;
+    }
+
+    @Override
+    public boolean hasNext() {
+        if(!initialized){
+            fetch(new TextPageLink(fetchSize));
+            initialized = true;
+        }
+        if(currentIdx == currentItems.size()){
+            if(hasNextPack){
+                fetch(nextPackLink);
+            }
+        }
+        return currentIdx != currentItems.size();
+    }
+
+    private void fetch(TextPageLink link) {
+        TextPageData<T> pageData = function.fetch(link);
+        currentIdx = 0;
+        currentItems = pageData.getData();
+        hasNextPack = pageData.hasNext();
+        nextPackLink = pageData.getNextPageLink();
+    }
+
+    @Override
+    public T next() {
+        return currentItems.get(currentIdx++);
+    }
+
+    public static interface FetchFunction<T extends SearchTextBased<? extends UUIDBased>> {
+
+        TextPageData<T> fetch(TextPageLink link);
+
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java
new file mode 100644
index 0000000..cc9d22d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageData.java
@@ -0,0 +1,71 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.page;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class TextPageData<T extends SearchTextBased<? extends UUIDBased>> {
+
+    private final List<T> data;
+    private final TextPageLink nextPageLink;
+    private final boolean hasNext;
+
+    public TextPageData(List<T> data, TextPageLink pageLink) {
+        super();
+        this.data = data;
+        int limit = pageLink.getLimit();
+        if (data != null && data.size() == limit) {
+            int index = data.size()-1;
+            UUID idOffset = data.get(index).getId().getId();
+            String textOffset = data.get(index).getSearchText();
+            nextPageLink = new TextPageLink(limit, pageLink.getTextSearch(), idOffset, textOffset);
+            hasNext = true;
+        } else {
+            nextPageLink = null;
+            hasNext = false;
+        }
+    }
+    
+    @JsonCreator
+    public TextPageData(@JsonProperty("data") List<T> data,
+                        @JsonProperty("nextPageLink") TextPageLink nextPageLink,
+                        @JsonProperty("hasNext") boolean hasNext) {
+        this.data = data;
+        this.nextPageLink = nextPageLink;
+        this.hasNext = hasNext;
+    }
+
+    public List<T> getData() {
+        return data;
+    }
+
+    @JsonProperty("hasNext")
+    public boolean hasNext() {
+        return hasNext;
+    }
+    
+    public TextPageLink getNextPageLink() {
+        return nextPageLink;
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java
new file mode 100644
index 0000000..d91b90f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TextPageLink.java
@@ -0,0 +1,80 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.page;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.UUID;
+
+@ToString
+public class TextPageLink extends BasePageLink implements Serializable {
+
+    private static final long serialVersionUID = -4189954843653250480L;
+
+    @Getter private final String textSearch;
+    @Getter private final String textSearchBound;
+    @Getter private final String textOffset;
+
+    public TextPageLink(int limit) {
+        this(limit, null, null, null);
+    }
+
+    public TextPageLink(int limit, String textSearch) {
+        this(limit, textSearch, null, null);
+    }
+
+    public TextPageLink(int limit, String textSearch, UUID idOffset, String textOffset) {
+        super(limit, idOffset);
+        this.textSearch = textSearch != null ? textSearch.toLowerCase() : null;
+        this.textSearchBound = nextSequence(this.textSearch);
+        this.textOffset = textOffset != null ? textOffset.toLowerCase() : null;
+    }
+
+    @JsonCreator
+    public TextPageLink(@JsonProperty("limit") int limit,
+                        @JsonProperty("textSearch") String textSearch,
+                        @JsonProperty("textSearchBound") String textSearchBound,
+                        @JsonProperty("textOffset") String textOffset,
+                        @JsonProperty("idOffset") UUID idOffset) {
+        super(limit, idOffset);
+        this.textSearch = textSearch;
+        this.textSearchBound = textSearchBound;
+        this.textOffset = textOffset;
+        this.idOffset = idOffset;
+    }
+
+    private static String nextSequence(String input) {
+        if (input != null && input.length() > 0) {
+            char[] chars = input.toCharArray();
+            int i = chars.length - 1;
+            while (i >= 0 && ++chars[i--] == Character.MIN_VALUE) ;
+            if (i == -1 && (chars.length == 0 || chars[0] == Character.MIN_VALUE)) {
+                char buf[] = Arrays.copyOf(input.toCharArray(), input.length() + 1);
+                buf[buf.length - 1] = Character.MIN_VALUE;
+                return new String(buf);
+            }
+            return new String(chars);
+        } else {
+            return null;
+        }
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java
new file mode 100644
index 0000000..6ed927e
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageData.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.page;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import java.util.List;
+import java.util.UUID;
+
+public class TimePageData<T extends BaseData<? extends UUIDBased>> {
+
+    private final List<T> data;
+    private final TimePageLink nextPageLink;
+    private final boolean hasNext;
+
+    public TimePageData(List<T> data, TimePageLink pageLink) {
+        super();
+        this.data = data;
+        int limit = pageLink.getLimit();
+        if (data != null && data.size() == limit) {
+            int index = data.size() - 1;
+            UUID idOffset = data.get(index).getId().getId();
+            nextPageLink = new TimePageLink(limit, pageLink.getStartTime(), pageLink.getEndTime(), pageLink.isAscOrder(), idOffset);
+            hasNext = true;
+        } else {
+            nextPageLink = null;
+            hasNext = false;
+        }
+    }
+
+    @JsonCreator
+    public TimePageData(@JsonProperty("data") List<T> data,
+                        @JsonProperty("nextPageLink") TimePageLink nextPageLink,
+                        @JsonProperty("hasNext") boolean hasNext) {
+        this.data = data;
+        this.nextPageLink = nextPageLink;
+        this.hasNext = hasNext;
+    }
+
+    public List<T> getData() {
+        return data;
+    }
+
+    @JsonProperty("hasNext")
+    public boolean hasNext() {
+        return hasNext;
+    }
+
+    public TimePageLink getNextPageLink() {
+        return nextPageLink;
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java
new file mode 100644
index 0000000..5296a7f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/page/TimePageLink.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.page;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Getter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.UUID;
+
+@ToString
+public class TimePageLink extends BasePageLink implements Serializable {
+
+    private static final long serialVersionUID = -4189954843653250480L;
+
+    @Getter private final Long startTime;
+    @Getter private final Long endTime;
+    @Getter private final boolean ascOrder;
+
+    public TimePageLink(int limit) {
+        this(limit, null, null, false, null);
+    }
+
+    public TimePageLink(int limit, Long startTime) {
+        this(limit, startTime, null, false, null);
+    }
+
+    public TimePageLink(int limit, Long startTime, Long endTime) {
+        this(limit, startTime, endTime, false, null);
+    }
+
+    public TimePageLink(int limit, Long startTime, Long endTime, boolean ascOrder) {
+        this(limit, startTime, endTime, ascOrder, null);
+    }
+
+    @JsonCreator
+    public TimePageLink(@JsonProperty("limit") int limit,
+                        @JsonProperty("startTime") Long startTime,
+                        @JsonProperty("endTime") Long endTime,
+                        @JsonProperty("ascOrder") boolean ascOrder,
+                        @JsonProperty("idOffset") UUID idOffset) {
+        super(limit, idOffset);
+        this.startTime = startTime;
+        this.endTime = endTime;
+        this.ascOrder = ascOrder;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
new file mode 100644
index 0000000..dac6ef7
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.plugin;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.*;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.ComponentDescriptorId;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public class ComponentDescriptor extends SearchTextBased<ComponentDescriptorId> {
+
+    private static final long serialVersionUID = 1L;
+
+    @Getter @Setter private ComponentType type;
+    @Getter @Setter private ComponentScope scope;
+    @Getter @Setter private String name;
+    @Getter @Setter private String clazz;
+    @Getter @Setter private JsonNode configurationDescriptor;
+    @Getter @Setter private String actions;
+
+    public ComponentDescriptor() {
+        super();
+    }
+
+    public ComponentDescriptor(ComponentDescriptorId id) {
+        super(id);
+    }
+
+    public ComponentDescriptor(ComponentDescriptor plugin) {
+        super(plugin);
+        this.type = plugin.getType();
+        this.scope = plugin.getScope();
+        this.name = plugin.getName();
+        this.clazz = plugin.getClazz();
+        this.configurationDescriptor = plugin.getConfigurationDescriptor();
+        this.actions = plugin.getActions();
+    }
+
+    @Override
+    public String getSearchText() {
+        return name;
+    }
+
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        ComponentDescriptor that = (ComponentDescriptor) o;
+
+        if (type != that.type) return false;
+        if (scope != that.scope) return false;
+        if (name != null ? !name.equals(that.name) : that.name != null) return false;
+        if (actions != null ? !actions.equals(that.actions) : that.actions != null) return false;
+        if (configurationDescriptor != null ? !configurationDescriptor.equals(that.configurationDescriptor) : that.configurationDescriptor != null) return false;
+        return clazz != null ? clazz.equals(that.clazz) : that.clazz == null;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (type != null ? type.hashCode() : 0);
+        result = 31 * result + (scope != null ? scope.hashCode() : 0);
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + (clazz != null ? clazz.hashCode() : 0);
+        result = 31 * result + (actions != null ? actions.hashCode() : 0);
+        return result;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java
new file mode 100644
index 0000000..4126b52
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleEvent.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.plugin;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentLifecycleEvent implements Serializable {
+    CREATED, STARTED, ACTIVATED, SUSPENDED, UPDATED, STOPPED, DELETED
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java
new file mode 100644
index 0000000..0ee6736
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentLifecycleState.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.plugin;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentLifecycleState {
+    ACTIVE, SUSPENDED
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentScope.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentScope.java
new file mode 100644
index 0000000..332563b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentScope.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.plugin;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentScope {
+    SYSTEM, TENANT
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
new file mode 100644
index 0000000..439a1d8
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentType.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.plugin;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum ComponentType {
+
+    FILTER, PROCESSOR, ACTION, PLUGIN
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
new file mode 100644
index 0000000..7cad18f
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/plugin/PluginMetaData.java
@@ -0,0 +1,175 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.plugin;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class PluginMetaData extends SearchTextBased<PluginId> {
+
+    private static final long serialVersionUID = 1L;
+
+    private String apiToken;
+    private TenantId tenantId;
+    private String name;
+    private String clazz;
+    private boolean publicAccess;
+    private ComponentLifecycleState state;
+    private JsonNode configuration;
+    private JsonNode additionalInfo;
+
+    public PluginMetaData() {
+        super();
+    }
+
+    public PluginMetaData(PluginId id) {
+        super(id);
+    }
+
+    public PluginMetaData(PluginMetaData plugin) {
+        super(plugin);
+        this.apiToken = plugin.getApiToken();
+        this.tenantId = plugin.getTenantId();
+        this.name = plugin.getName();
+        this.clazz = plugin.getClazz();
+        this.publicAccess = plugin.isPublicAccess();
+        this.state = plugin.getState();
+        this.configuration = plugin.getConfiguration();
+        this.additionalInfo = plugin.getAdditionalInfo();
+    }
+
+    @Override
+    public String getSearchText() {
+        return name;
+    }
+
+    public String getApiToken() {
+        return apiToken;
+    }
+
+    public void setApiToken(String apiToken) {
+        this.apiToken = apiToken;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getClazz() {
+        return clazz;
+    }
+
+    public void setClazz(String clazz) {
+        this.clazz = clazz;
+    }
+
+    public JsonNode getConfiguration() {
+        return configuration;
+    }
+
+    public void setConfiguration(JsonNode configuration) {
+        this.configuration = configuration;
+    }
+
+    public boolean isPublicAccess() {
+        return publicAccess;
+    }
+
+    public void setPublicAccess(boolean publicAccess) {
+        this.publicAccess = publicAccess;
+    }
+
+    public void setState(ComponentLifecycleState state) {
+        this.state = state;
+    }
+
+    public ComponentLifecycleState getState() {
+        return state;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((apiToken == null) ? 0 : apiToken.hashCode());
+        result = prime * result + ((clazz == null) ? 0 : clazz.hashCode());
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        PluginMetaData other = (PluginMetaData) obj;
+        if (apiToken == null) {
+            if (other.apiToken != null)
+                return false;
+        } else if (!apiToken.equals(other.apiToken))
+            return false;
+        if (clazz == null) {
+            if (other.clazz != null)
+                return false;
+        } else if (!clazz.equals(other.clazz))
+            return false;
+        if (name == null) {
+            if (other.name != null)
+                return false;
+        } else if (!name.equals(other.name))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "PluginMetaData [apiToken=" + apiToken + ", tenantId=" + tenantId + ", name=" + name + ", clazz=" + clazz + ", publicAccess=" + publicAccess
+                + ", configuration=" + configuration + "]";
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
new file mode 100644
index 0000000..3ed132d
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleMetaData.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.rule;
+
+import lombok.Data;
+import lombok.ToString;
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+
+@Data
+public class RuleMetaData extends SearchTextBased<RuleId> {
+
+    private static final long serialVersionUID = -5656679015122935465L;
+
+    private TenantId tenantId;
+    private String name;
+    private ComponentLifecycleState state;
+    private int weight;
+    private String pluginToken;
+    private JsonNode filters;
+    private JsonNode processor;
+    private JsonNode action;
+    private JsonNode additionalInfo;
+
+    public RuleMetaData() {
+        super();
+    }
+
+    public RuleMetaData(RuleId id) {
+        super(id);
+    }
+
+    public RuleMetaData(RuleMetaData rule) {
+        super(rule);
+        this.tenantId = rule.getTenantId();
+        this.name = rule.getName();
+        this.state = rule.getState();
+        this.weight = rule.getWeight();
+        this.pluginToken = rule.getPluginToken();
+        this.filters = rule.getFilters();
+        this.processor = rule.getProcessor();
+        this.action = rule.getAction();
+        this.additionalInfo = rule.getAdditionalInfo();
+    }
+
+    @Override
+    public String getSearchText() {
+        return name;
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleType.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleType.java
new file mode 100644
index 0000000..f5b0700
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleType.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.rule;
+
+/**
+ * Defines origin of the rule.
+ * 
+ * @author ashvayka
+ *
+ */
+public enum RuleType {
+    
+    SYSTEM, USER;
+    
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/rule/Scope.java b/common/data/src/main/java/org/thingsboard/server/common/data/rule/Scope.java
new file mode 100644
index 0000000..e8a9870
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/rule/Scope.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.rule;
+
+/**
+ * Defines scope of the rule execution in the actor system
+ * 
+ * @author ashvayka
+ *
+ */
+public enum Scope {
+
+    SYSTEM, TENANT, CUSTOMER, DEVICE, RULE;
+
+}
\ No newline at end of file
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBased.java
new file mode 100644
index 0000000..c63d8ca
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/SearchTextBased.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.UUIDBased;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public abstract class SearchTextBased<I extends UUIDBased> extends BaseData<I> {
+
+    private static final long serialVersionUID = -539812997348227609L;
+    
+    public SearchTextBased() {
+        super();
+    }
+
+    public SearchTextBased(I id) {
+        super(id);
+    }
+    
+    public SearchTextBased(SearchTextBased<I> searchTextBased) {
+        super(searchTextBased);
+    }
+    
+    @JsonIgnore
+    public abstract String getSearchText(); 
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
new file mode 100644
index 0000000..568d07b
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.security;
+
+public enum Authority {
+    
+    SYS_ADMIN(0),
+    TENANT_ADMIN(1),
+    CUSTOMER_USER(2),
+    REFRESH_TOKEN(10);
+
+    private int code;
+
+    Authority(int code) {
+        this.code = code;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public static Authority parse(String value) {
+        Authority authority = null;
+        if (value != null && value.length() != 0) {
+            for (Authority current : Authority.values()) {
+                if (current.name().equalsIgnoreCase(value)) {
+                    authority = current;
+                    break;
+                }
+            }
+        }
+        return authority;
+    }
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
new file mode 100644
index 0000000..a64d324
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentials.java
@@ -0,0 +1,128 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.security;
+
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.DeviceCredentialsId;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+public class DeviceCredentials extends BaseData<DeviceCredentialsId> implements DeviceCredentialsFilter {
+
+    private static final long serialVersionUID = -7869261127032877765L;
+    
+    private DeviceId deviceId;
+    private DeviceCredentialsType credentialsType;
+    private String credentialsId;
+    private String credentialsValue;
+    
+    public DeviceCredentials() {
+        super();
+    }
+
+    public DeviceCredentials(DeviceCredentialsId id) {
+        super(id);
+    }
+
+    public DeviceCredentials(DeviceCredentials deviceCredentials) {
+        super(deviceCredentials);
+        this.deviceId = deviceCredentials.getDeviceId();
+        this.credentialsType = deviceCredentials.getCredentialsType();
+        this.credentialsId = deviceCredentials.getCredentialsId();
+        this.credentialsValue = deviceCredentials.getCredentialsValue();
+    }
+
+    public DeviceId getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(DeviceId deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    @Override
+    public DeviceCredentialsType getCredentialsType() {
+        return credentialsType;
+    }
+
+    public void setCredentialsType(DeviceCredentialsType credentialsType) {
+        this.credentialsType = credentialsType;
+    }
+
+    @Override
+    public String getCredentialsId() {
+        return credentialsId;
+    }
+
+    public void setCredentialsId(String credentialsId) {
+        this.credentialsId = credentialsId;
+    }
+
+    public String getCredentialsValue() {
+        return credentialsValue;
+    }
+
+    public void setCredentialsValue(String credentialsValue) {
+        this.credentialsValue = credentialsValue;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((credentialsId == null) ? 0 : credentialsId.hashCode());
+        result = prime * result + ((credentialsType == null) ? 0 : credentialsType.hashCode());
+        result = prime * result + ((credentialsValue == null) ? 0 : credentialsValue.hashCode());
+        result = prime * result + ((deviceId == null) ? 0 : deviceId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        DeviceCredentials other = (DeviceCredentials) obj;
+        if (credentialsId == null) {
+            if (other.credentialsId != null)
+                return false;
+        } else if (!credentialsId.equals(other.credentialsId))
+            return false;
+        if (credentialsType != other.credentialsType)
+            return false;
+        if (credentialsValue == null) {
+            if (other.credentialsValue != null)
+                return false;
+        } else if (!credentialsValue.equals(other.credentialsValue))
+            return false;
+        if (deviceId == null) {
+            if (other.deviceId != null)
+                return false;
+        } else if (!deviceId.equals(other.deviceId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceCredentials [deviceId=" + deviceId + ", credentialsType=" + credentialsType + ", credentialsId="
+                + credentialsId + ", credentialsValue=" + credentialsValue + ", createdTime=" + createdTime + ", id="
+                + id + "]";
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java
new file mode 100644
index 0000000..c4b4dcc
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsFilter.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.security;
+
+/**
+ * TODO: This is a temporary name. DeviceCredentialsId is resereved in dao layer
+ */
+public interface DeviceCredentialsFilter {
+
+    String getCredentialsId();
+
+    DeviceCredentialsType getCredentialsType();
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java
new file mode 100644
index 0000000..3daa1e4
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceCredentialsType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.security;
+
+public enum DeviceCredentialsType {
+
+    ACCESS_TOKEN
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java
new file mode 100644
index 0000000..8ce9f00
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/DeviceTokenCredentials.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.security;
+
+public class DeviceTokenCredentials implements DeviceCredentialsFilter {
+
+    private final String token;
+
+    public DeviceTokenCredentials(String token) {
+        super();
+        this.token = token;
+    }
+
+    @Override
+    public DeviceCredentialsType getCredentialsType() {
+        return DeviceCredentialsType.ACCESS_TOKEN;
+    }
+
+    @Override
+    public String getCredentialsId() {
+        return token;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceTokenCredentials [token=" + token + "]";
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
new file mode 100644
index 0000000..42d90bb
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java
@@ -0,0 +1,156 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.security;
+
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.UserCredentialsId;
+import org.thingsboard.server.common.data.id.UserId;
+
+public class UserCredentials extends BaseData<UserCredentialsId> {
+
+    private static final long serialVersionUID = -2108436378880529163L;
+
+    private UserId userId;
+    private boolean enabled;
+    private String password;
+    private String activateToken;
+    private String resetToken;
+    
+    public UserCredentials() {
+        super();
+    }
+
+    public UserCredentials(UserCredentialsId id) {
+        super(id);
+    }
+
+    public UserCredentials(UserCredentials userCredentials) {
+        super(userCredentials);
+        this.userId = userCredentials.getUserId();
+        this.password = userCredentials.getPassword();
+        this.enabled = userCredentials.isEnabled();
+        this.activateToken = userCredentials.getActivateToken();
+        this.resetToken = userCredentials.getResetToken();
+    }
+
+    public UserId getUserId() {
+        return userId;
+    }
+
+    public void setUserId(UserId userId) {
+        this.userId = userId;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getActivateToken() {
+        return activateToken;
+    }
+
+    public void setActivateToken(String activateToken) {
+        this.activateToken = activateToken;
+    }
+    
+    public String getResetToken() {
+        return resetToken;
+    }
+
+    public void setResetToken(String resetToken) {
+        this.resetToken = resetToken;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((activateToken == null) ? 0 : activateToken.hashCode());
+        result = prime * result + (enabled ? 1231 : 1237);
+        result = prime * result + ((password == null) ? 0 : password.hashCode());
+        result = prime * result + ((resetToken == null) ? 0 : resetToken.hashCode());
+        result = prime * result + ((userId == null) ? 0 : userId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        UserCredentials other = (UserCredentials) obj;
+        if (activateToken == null) {
+            if (other.activateToken != null)
+                return false;
+        } else if (!activateToken.equals(other.activateToken))
+            return false;
+        if (enabled != other.enabled)
+            return false;
+        if (password == null) {
+            if (other.password != null)
+                return false;
+        } else if (!password.equals(other.password))
+            return false;
+        if (resetToken == null) {
+            if (other.resetToken != null)
+                return false;
+        } else if (!resetToken.equals(other.resetToken))
+            return false;
+        if (userId == null) {
+            if (other.userId != null)
+                return false;
+        } else if (!userId.equals(other.userId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("UserCredentials [userId=");
+        builder.append(userId);
+        builder.append(", enabled=");
+        builder.append(enabled);
+        builder.append(", password=");
+        builder.append(password);
+        builder.append(", activateToken=");
+        builder.append(activateToken);
+        builder.append(", resetToken=");
+        builder.append(resetToken);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
new file mode 100644
index 0000000..2c22967
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java
@@ -0,0 +1,144 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class Tenant extends ContactBased<TenantId>{
+
+    private static final long serialVersionUID = 8057243243859922101L;
+    
+    private String title;
+    private String region;
+    private JsonNode additionalInfo;
+    
+    public Tenant() {
+        super();
+    }
+
+    public Tenant(TenantId id) {
+        super(id);
+    }
+    
+    public Tenant(Tenant tenant) {
+        super(tenant);
+        this.title = tenant.getTitle();
+        this.region = tenant.getRegion();
+        this.additionalInfo = tenant.getAdditionalInfo();
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public String getRegion() {
+        return region;
+    }
+
+    public void setRegion(String region) {
+        this.region = region;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+    
+    @Override
+    public String getSearchText() {
+        return title;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+        result = prime * result + ((region == null) ? 0 : region.hashCode());
+        result = prime * result + ((title == null) ? 0 : title.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        Tenant other = (Tenant) obj;
+        if (additionalInfo == null) {
+            if (other.additionalInfo != null)
+                return false;
+        } else if (!additionalInfo.equals(other.additionalInfo))
+            return false;
+        if (region == null) {
+            if (other.region != null)
+                return false;
+        } else if (!region.equals(other.region))
+            return false;
+        if (title == null) {
+            if (other.title != null)
+                return false;
+        } else if (!title.equals(other.title))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Tenant [title=");
+        builder.append(title);
+        builder.append(", region=");
+        builder.append(region);
+        builder.append(", additionalInfo=");
+        builder.append(additionalInfo);
+        builder.append(", country=");
+        builder.append(country);
+        builder.append(", state=");
+        builder.append(state);
+        builder.append(", city=");
+        builder.append(city);
+        builder.append(", address=");
+        builder.append(address);
+        builder.append(", address2=");
+        builder.append(address2);
+        builder.append(", zip=");
+        builder.append(zip);
+        builder.append(", phone=");
+        builder.append(phone);
+        builder.append(", email=");
+        builder.append(email);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/User.java b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
new file mode 100644
index 0000000..2124454
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/User.java
@@ -0,0 +1,200 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.UserId;
+import org.thingsboard.server.common.data.security.Authority;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+public class User extends SearchTextBased<UserId> {
+
+    private static final long serialVersionUID = 8250339805336035966L;
+
+    private TenantId tenantId;
+    private CustomerId customerId;
+    private String email;
+    private Authority authority;
+    private String firstName;
+    private String lastName;
+    private JsonNode additionalInfo;
+
+    public User() {
+        super();
+    }
+
+    public User(UserId id) {
+        super(id);
+    }
+
+    public User(User user) {
+        super(user);
+        this.tenantId = user.getTenantId();
+        this.customerId = user.getCustomerId();
+        this.email = user.getEmail();
+        this.authority = user.getAuthority();
+        this.firstName = user.getFirstName();
+        this.lastName = user.getLastName();
+        this.additionalInfo = user.getAdditionalInfo();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    public void setCustomerId(CustomerId customerId) {
+        this.customerId = customerId;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public Authority getAuthority() {
+        return authority;
+    }
+
+    public void setAuthority(Authority authority) {
+        this.authority = authority;
+    }
+
+    public String getFirstName() {
+        return firstName;
+    }
+
+    public void setFirstName(String firstName) {
+        this.firstName = firstName;
+    }
+
+    public String getLastName() {
+        return lastName;
+    }
+
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
+
+    public JsonNode getAdditionalInfo() {
+        return additionalInfo;
+    }
+
+    public void setAdditionalInfo(JsonNode additionalInfo) {
+        this.additionalInfo = additionalInfo;
+    }
+    
+    @Override
+    public String getSearchText() {
+        return email;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = super.hashCode();
+        result = prime * result + ((additionalInfo == null) ? 0 : additionalInfo.hashCode());
+        result = prime * result + ((authority == null) ? 0 : authority.hashCode());
+        result = prime * result + ((customerId == null) ? 0 : customerId.hashCode());
+        result = prime * result + ((email == null) ? 0 : email.hashCode());
+        result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
+        result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
+        result = prime * result + ((tenantId == null) ? 0 : tenantId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (!super.equals(obj))
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        User other = (User) obj;
+        if (additionalInfo == null) {
+            if (other.additionalInfo != null)
+                return false;
+        } else if (!additionalInfo.equals(other.additionalInfo))
+            return false;
+        if (authority != other.authority)
+            return false;
+        if (customerId == null) {
+            if (other.customerId != null)
+                return false;
+        } else if (!customerId.equals(other.customerId))
+            return false;
+        if (email == null) {
+            if (other.email != null)
+                return false;
+        } else if (!email.equals(other.email))
+            return false;
+        if (firstName == null) {
+            if (other.firstName != null)
+                return false;
+        } else if (!firstName.equals(other.firstName))
+            return false;
+        if (lastName == null) {
+            if (other.lastName != null)
+                return false;
+        } else if (!lastName.equals(other.lastName))
+            return false;
+        if (tenantId == null) {
+            if (other.tenantId != null)
+                return false;
+        } else if (!tenantId.equals(other.tenantId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("User [tenantId=");
+        builder.append(tenantId);
+        builder.append(", customerId=");
+        builder.append(customerId);
+        builder.append(", email=");
+        builder.append(email);
+        builder.append(", authority=");
+        builder.append(authority);
+        builder.append(", firstName=");
+        builder.append(firstName);
+        builder.append(", lastName=");
+        builder.append(lastName);
+        builder.append(", additionalInfo=");
+        builder.append(additionalInfo);
+        builder.append(", createdTime=");
+        builder.append(createdTime);
+        builder.append(", id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
new file mode 100644
index 0000000..65a7f72
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.widget;
+
+import org.thingsboard.server.common.data.SearchTextBased;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetsBundleId;
+
+import java.util.Arrays;
+
+public class WidgetsBundle extends SearchTextBased<WidgetsBundleId> {
+
+    private static final long serialVersionUID = -7627368878362410489L;
+
+    private TenantId tenantId;
+    private String alias;
+    private String title;
+    private byte[] image;
+
+    public WidgetsBundle() {
+        super();
+    }
+
+    public WidgetsBundle(WidgetsBundleId id) {
+        super(id);
+    }
+
+    public WidgetsBundle(WidgetsBundle widgetsBundle) {
+        super(widgetsBundle);
+        this.tenantId = widgetsBundle.getTenantId();
+        this.alias = widgetsBundle.getAlias();
+        this.title = widgetsBundle.getTitle();
+        this.image = widgetsBundle.getImage();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
+    public String getTitle() {
+        return title;
+    }
+
+    public void setTitle(String title) {
+        this.title = title;
+    }
+
+    public byte[] getImage() {
+        return image;
+    }
+
+    public void setImage(byte[] image) {
+        this.image = image;
+    }
+
+    @Override
+    public String getSearchText() {
+        return title;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        result = 31 * result + (alias != null ? alias.hashCode() : 0);
+        result = 31 * result + (title != null ? title.hashCode() : 0);
+        result = 31 * result + Arrays.hashCode(image);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+
+        WidgetsBundle that = (WidgetsBundle) o;
+
+        if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
+        if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
+        if (title != null ? !title.equals(that.title) : that.title != null) return false;
+        return Arrays.equals(image, that.image);
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("WidgetsBundle{");
+        sb.append("tenantId=").append(tenantId);
+        sb.append(", alias='").append(alias).append('\'');
+        sb.append(", title='").append(title).append('\'');
+        sb.append(", image=").append(Arrays.toString(image));
+        sb.append('}');
+        return sb.toString();
+    }
+
+}
diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
new file mode 100644
index 0000000..2f2851c
--- /dev/null
+++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetType.java
@@ -0,0 +1,129 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.data.widget;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.thingsboard.server.common.data.BaseData;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.id.WidgetTypeId;
+
+public class WidgetType extends BaseData<WidgetTypeId> {
+
+    private static final long serialVersionUID = 8388684344603660756L;
+
+    private TenantId tenantId;
+    private String bundleAlias;
+    private String alias;
+    private String name;
+    private JsonNode descriptor;
+
+    public WidgetType() {
+        super();
+    }
+
+    public WidgetType(WidgetTypeId id) {
+        super(id);
+    }
+
+    public WidgetType(WidgetType widgetType) {
+        super(widgetType);
+        this.tenantId = widgetType.getTenantId();
+        this.bundleAlias = widgetType.getBundleAlias();
+        this.alias = widgetType.getAlias();
+        this.name = widgetType.getName();
+        this.descriptor = widgetType.getDescriptor();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public void setTenantId(TenantId tenantId) {
+        this.tenantId = tenantId;
+    }
+
+    public String getBundleAlias() {
+        return bundleAlias;
+    }
+
+    public void setBundleAlias(String bundleAlias) {
+        this.bundleAlias = bundleAlias;
+    }
+
+    public String getAlias() {
+        return alias;
+    }
+
+    public void setAlias(String alias) {
+        this.alias = alias;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public JsonNode getDescriptor() {
+        return descriptor;
+    }
+
+    public void setDescriptor(JsonNode descriptor) {
+        this.descriptor = descriptor;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = super.hashCode();
+        result = 31 * result + (tenantId != null ? tenantId.hashCode() : 0);
+        result = 31 * result + (bundleAlias != null ? bundleAlias.hashCode() : 0);
+        result = 31 * result + (alias != null ? alias.hashCode() : 0);
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        result = 31 * result + (descriptor != null ? descriptor.hashCode() : 0);
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        if (!super.equals(o)) return false;
+
+        WidgetType that = (WidgetType) o;
+
+        if (tenantId != null ? !tenantId.equals(that.tenantId) : that.tenantId != null) return false;
+        if (bundleAlias != null ? !bundleAlias.equals(that.bundleAlias) : that.bundleAlias != null) return false;
+        if (alias != null ? !alias.equals(that.alias) : that.alias != null) return false;
+        if (name != null ? !name.equals(that.name) : that.name != null) return false;
+        return descriptor != null ? descriptor.equals(that.descriptor) : that.descriptor == null;
+
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder("WidgetType{");
+        sb.append("tenantId=").append(tenantId);
+        sb.append(", bundleAlias='").append(bundleAlias).append('\'');
+        sb.append(", alias='").append(alias).append('\'');
+        sb.append(", name='").append(name).append('\'');
+        sb.append(", descriptor=").append(descriptor);
+        sb.append('}');
+        return sb.toString();
+    }
+
+}
diff --git a/common/message/pom.xml b/common/message/pom.xml
new file mode 100644
index 0000000..be14928
--- /dev/null
+++ b/common/message/pom.xml
@@ -0,0 +1,76 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard.server</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>common</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server.common</groupId>
+    <artifactId>message</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Common Messages</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>data</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+        </plugins>
+    </build>
+
+</project>
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java
new file mode 100644
index 0000000..d3911be
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/CustomerAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+
+public interface CustomerAwareMsg {
+
+	CustomerId getCustomerId();
+	
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/DeviceAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/DeviceAwareMsg.java
new file mode 100644
index 0000000..2d1c85b
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/DeviceAwareMsg.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.DeviceId;
+
+public interface DeviceAwareMsg {
+
+    DeviceId getDeviceId();
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/NodeAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/NodeAwareMsg.java
new file mode 100644
index 0000000..64ea606
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/NodeAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.NodeId;
+
+public interface NodeAwareMsg {
+
+	NodeId getNodeId();
+	
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/PluginAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/PluginAwareMsg.java
new file mode 100644
index 0000000..c0b4a26
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/PluginAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.PluginId;
+
+public interface PluginAwareMsg {
+
+    PluginId getPluginId();
+    
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleAwareMsg.java
new file mode 100644
index 0000000..43631d9
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/RuleAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.RuleId;
+
+public interface RuleAwareMsg {
+
+	RuleId getRuleId();
+	
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java
new file mode 100644
index 0000000..c4b7856
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/SessionAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public interface SessionAwareMsg {
+
+    SessionId getSessionId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java
new file mode 100644
index 0000000..5a4cc97
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/aware/TenantAwareMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.aware;
+
+import org.thingsboard.server.common.data.id.TenantId;
+
+public interface TenantAwareMsg {
+
+	TenantId getTenantId();
+	
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
new file mode 100644
index 0000000..dba41f7
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ClusterEventMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.cluster;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public final class ClusterEventMsg {
+
+    private final ServerAddress serverAddress;
+    private final boolean added;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java
new file mode 100644
index 0000000..d574bac
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ServerAddress.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.cluster;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@EqualsAndHashCode
+public class ServerAddress implements Comparable<ServerAddress>, Serializable {
+
+    private final String host;
+    private final int port;
+
+    @Override
+    public int compareTo(ServerAddress o) {
+        int result = this.host.compareTo(o.host);
+        if (result == 0) {
+            result = this.port - o.port;
+        }
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return '[' + host + ':' + port + ']';
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
new file mode 100644
index 0000000..9aae56b
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/cluster/ToAllNodesMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.cluster;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ToAllNodesMsg extends Serializable {
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
new file mode 100644
index 0000000..f782873
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesSubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class AttributesSubscribeMsg implements FromDeviceMsg {
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
new file mode 100644
index 0000000..a38a4cb
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUnsubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class AttributesUnsubscribeMsg implements FromDeviceMsg {
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
new file mode 100644
index 0000000..e3a92ca
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/AttributesUpdateNotification.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+@ToString
+public class AttributesUpdateNotification implements ToDeviceMsg {
+
+    private static final long serialVersionUID = 1L;
+
+    private AttributesKVMsg data;
+
+    public AttributesUpdateNotification(AttributesKVMsg data) {
+        this.data = data;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return true;
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.ATTRIBUTES_UPDATE_NOTIFICATION;
+    }
+
+    public AttributesKVMsg getData() {
+        return data;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
new file mode 100644
index 0000000..82e4377
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicCommandAckResponse.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.MsgType;
+
+public class BasicCommandAckResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
+
+    private static final long serialVersionUID = 1L;
+
+    public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+        return BasicCommandAckResponse.onSuccess(requestMsgType, requestId, 200);
+    }
+
+    public static BasicCommandAckResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+        return new BasicCommandAckResponse(requestMsgType, requestId, true, null, code);
+    }
+
+    public static BasicCommandAckResponse onError(MsgType requestMsgType, Integer requestId, Exception error) {
+        return new BasicCommandAckResponse(requestMsgType, requestId, false, error, null);
+    }
+
+    private BasicCommandAckResponse(MsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
+        super(requestMsgType, requestId, MsgType.TO_DEVICE_RPC_RESPONSE_ACK, success, error, code);
+    }
+
+    @Override
+    public String toString() {
+        return "BasicStatusCodeResponse []";
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
new file mode 100644
index 0000000..a8fdc86
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesRequest.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+import java.util.Set;
+
+@ToString
+public class BasicGetAttributesRequest extends BasicRequest implements GetAttributesRequest {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Set<String> clientKeys;
+    private final Set<String> sharedKeys;
+
+    public BasicGetAttributesRequest(Integer requestId, Set<String> clientKeys, Set<String> sharedKeys) {
+        super(requestId);
+        this.clientKeys = clientKeys;
+        this.sharedKeys = sharedKeys;
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.GET_ATTRIBUTES_REQUEST;
+    }
+
+    @Override
+    public Set<String> getClientAttributeNames() {
+        return clientKeys;
+    }
+
+    @Override
+    public Set<String> getSharedAttributeNames() {
+        return sharedKeys;
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
new file mode 100644
index 0000000..ed786bc
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicGetAttributesResponse.java
@@ -0,0 +1,39 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+@ToString
+public class BasicGetAttributesResponse extends BasicResponseMsg<AttributesKVMsg> implements GetAttributesResponse {
+
+    private static final long serialVersionUID = 1L;
+
+    public static BasicGetAttributesResponse onSuccess(MsgType requestMsgType, int requestId, AttributesKVMsg code) {
+        return new BasicGetAttributesResponse(requestMsgType, requestId, true, null, code);
+    }
+
+    public static BasicGetAttributesResponse onError(MsgType requestMsgType, int requestId, Exception error) {
+        return new BasicGetAttributesResponse(requestMsgType, requestId, false, error, null);
+    }
+
+    private BasicGetAttributesResponse(MsgType requestMsgType, int requestId, boolean success, Exception error, AttributesKVMsg code) {
+        super(requestMsgType, requestId, MsgType.GET_ATTRIBUTES_RESPONSE, success, error, code);
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicRequest.java
new file mode 100644
index 0000000..26401d1
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicRequest.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class BasicRequest implements Serializable {
+
+    public static final Integer DEFAULT_REQUEST_ID = 0;
+
+    private final Integer requestId;
+
+    public BasicRequest(Integer requestId) {
+        this.requestId = requestId;
+    }
+
+    public Integer getRequestId() {
+        return requestId;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
new file mode 100644
index 0000000..2492192
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicResponseMsg.java
@@ -0,0 +1,79 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.thingsboard.server.common.msg.session.MsgType;
+
+
+public class BasicResponseMsg<T extends Serializable> implements ResponseMsg<T> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final MsgType requestMsgType;
+    private final Integer requestId;
+    private final MsgType msgType;
+    private final boolean success;
+    private final T data;
+    private final Exception error;
+
+    protected BasicResponseMsg(MsgType requestMsgType, Integer requestId, MsgType msgType, boolean success, Exception error, T data) {
+        super();
+        this.requestMsgType = requestMsgType;
+        this.requestId = requestId;
+        this.msgType = msgType;
+        this.success = success;
+        this.error = error;
+        this.data = data;
+    }
+
+    @Override
+    public MsgType getRequestMsgType() {
+        return requestMsgType;
+    }
+
+    @Override
+    public Integer getRequestId() {
+        return requestId;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return success;
+    }
+
+    @Override
+    public Optional<Exception> getError() {
+        return Optional.ofNullable(error);
+    }
+
+    @Override
+    public Optional<T> getData() {
+        return Optional.ofNullable(data);
+    }
+
+    @Override
+    public String toString() {
+        return "BasicResponseMsg [success=" + success + ", data=" + data + ", error=" + error + "]";
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return msgType;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
new file mode 100644
index 0000000..1c2bf41
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicStatusCodeResponse.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.ToString;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+@ToString
+public class BasicStatusCodeResponse extends BasicResponseMsg<Integer> implements StatusCodeResponse {
+
+    private static final long serialVersionUID = 1L;
+
+    public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId) {
+        return BasicStatusCodeResponse.onSuccess(requestMsgType, requestId, 0);
+    }
+
+    public static BasicStatusCodeResponse onSuccess(MsgType requestMsgType, Integer requestId, Integer code) {
+        return new BasicStatusCodeResponse(requestMsgType, requestId, true, null, code);
+    }
+
+    public static BasicStatusCodeResponse onError(MsgType requestMsgType, Integer requestId, Exception error) {
+        return new BasicStatusCodeResponse(requestMsgType, requestId, false, error, null);
+    }
+
+    private BasicStatusCodeResponse(MsgType requestMsgType, Integer requestId, boolean success, Exception error, Integer code) {
+        super(requestMsgType, requestId, MsgType.STATUS_CODE_RESPONSE, success, error, code);
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
new file mode 100644
index 0000000..d984e7f
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicTelemetryUploadRequest.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+public class BasicTelemetryUploadRequest extends BasicRequest implements TelemetryUploadRequest {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Map<Long, List<KvEntry>> data;
+
+    public BasicTelemetryUploadRequest() {
+        this(DEFAULT_REQUEST_ID);
+    }
+
+    public BasicTelemetryUploadRequest(Integer requestId) {
+        super(requestId);
+        this.data = new HashMap<>();
+    }
+
+    public void add(long ts, KvEntry entry) {
+        List<KvEntry> tsEntries = data.get(ts);
+        if (tsEntries == null) {
+            tsEntries = new ArrayList<>();
+            data.put(ts, tsEntries);
+        }
+        tsEntries.add(entry);
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.POST_TELEMETRY_REQUEST;
+    }
+
+    @Override
+    public Map<Long, List<KvEntry>> getData() {
+        return data;
+    }
+
+    @Override
+    public String toString() {
+        return "BasicTelemetryUploadRequest [data=" + data + "]";
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java
new file mode 100644
index 0000000..538959d
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicToDeviceSessionActorMsg.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+public class BasicToDeviceSessionActorMsg implements ToDeviceSessionActorMsg {
+
+    private final ToDeviceMsg msg;
+    private final SessionId sessionId;
+
+    public BasicToDeviceSessionActorMsg(ToDeviceMsg msg, SessionId sessionId) {
+        super();
+        this.msg = msg;
+        this.sessionId = sessionId;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return sessionId;
+    }
+
+    @Override
+    public ToDeviceMsg getMsg() {
+        return msg;
+    }
+
+    @Override
+    public String toString() {
+        return "BasicToSessionResponseMsg [msg=" + msg + ", sessionId=" + sessionId + "]";
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java
new file mode 100644
index 0000000..c968f04
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/BasicUpdateAttributesRequest.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+public class BasicUpdateAttributesRequest extends BasicRequest implements UpdateAttributesRequest {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Set<AttributeKvEntry> data;
+
+    public BasicUpdateAttributesRequest() {
+        this(DEFAULT_REQUEST_ID);
+    }
+
+    public BasicUpdateAttributesRequest(Integer requestId) {
+        super(requestId);
+        this.data = new LinkedHashSet<>();
+    }
+
+    public void add(AttributeKvEntry entry) {
+        this.data.add(entry);
+    }
+
+    public void add(Collection<AttributeKvEntry> entries) {
+        this.data.addAll(entries);
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.POST_ATTRIBUTES_REQUEST;
+    }
+
+    @Override
+    public Set<AttributeKvEntry> getAttributes() {
+        return data;
+    }
+
+    @Override
+    public String toString() {
+        return "BasicUpdateAttributesRequest [data=" + data + "]";
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
new file mode 100644
index 0000000..49bca53
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.util.Set;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+
+public interface GetAttributesRequest extends FromDeviceRequestMsg {
+
+    Set<String> getClientAttributeNames();
+    Set<String> getSharedAttributeNames();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java
new file mode 100644
index 0000000..ec3f074
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/GetAttributesResponse.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+
+public interface GetAttributesResponse extends ResponseMsg<AttributesKVMsg> {
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
new file mode 100644
index 0000000..861eab8
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ResponseMsg.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+public interface ResponseMsg<T extends Serializable> extends ToDeviceMsg {
+
+    MsgType getRequestMsgType();
+
+    Integer getRequestId();
+
+    Optional<Exception> getError();
+
+    Optional<T> getData();
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
new file mode 100644
index 0000000..b9ea578
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcSubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcSubscribeMsg implements FromDeviceMsg {
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
new file mode 100644
index 0000000..6934a95
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RpcUnsubscribeMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class RpcUnsubscribeMsg implements FromDeviceMsg {
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
new file mode 100644
index 0000000..1c235a0
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineError.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+/**
+ * @author Andrew Shvayka
+ */
+
+public enum RuleEngineError {
+
+    NO_RULES, NO_ACTIVE_RULES, NO_FILTERS_MATCHED, NO_REQUEST_FROM_ACTIONS, NO_TWO_WAY_ACTIONS, NO_RESPONSE_FROM_ACTIONS, PLUGIN_TIMEOUT(true);
+
+    private final boolean critical;
+
+    RuleEngineError() {
+        this(false);
+    }
+
+    RuleEngineError(boolean critical) {
+        this.critical = critical;
+    }
+
+    public boolean isCritical() {
+        return critical;
+    }
+
+    public int getPriority() {
+        return ordinal();
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
new file mode 100644
index 0000000..c9ea00a
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/RuleEngineErrorMsg.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class RuleEngineErrorMsg implements ToDeviceMsg {
+
+    private final MsgType inMsgType;
+    private final RuleEngineError error;
+
+    @Override
+    public boolean isSuccess() {
+        return false;
+    }
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.RULE_ENGINE_ERROR;
+    }
+
+    public String getErrorMsg() {
+        switch (error) {
+            case NO_RULES:
+                return "No rules configured!";
+            case NO_ACTIVE_RULES:
+                return "No active rules!";
+            case NO_FILTERS_MATCHED:
+                return "No rules that match current message!";
+            case NO_REQUEST_FROM_ACTIONS:
+                return "Rule filters match, but no plugin message produced by rule action!";
+            case NO_TWO_WAY_ACTIONS:
+                return "Rule filters match, but no rule with two-way action configured!";
+            case NO_RESPONSE_FROM_ACTIONS:
+                return "Rule filters match, message processed by plugin, but no response produced by rule action!";
+            case PLUGIN_TIMEOUT:
+                return "Timeout during processing of message by plugin!";
+            default:
+                throw new RuntimeException("Error " + error + " is not supported!");
+        }
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
new file mode 100644
index 0000000..61bc094
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/SessionCloseMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class SessionCloseMsg implements FromDeviceMsg {
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.SESSION_CLOSE;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java
new file mode 100644
index 0000000..81daf89
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/StatusCodeResponse.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+public interface StatusCodeResponse extends ResponseMsg<Integer>{
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java
new file mode 100644
index 0000000..0682d3b
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/TelemetryUploadRequest.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.util.List;
+import java.util.Map;
+
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+
+public interface TelemetryUploadRequest extends FromDeviceRequestMsg {
+
+    Map<Long, List<KvEntry>> getData();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
new file mode 100644
index 0000000..c376ea0
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcRequestMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequestMsg implements ToDeviceMsg {
+
+    private final int requestId;
+    private final String method;
+    private final String params;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.TO_DEVICE_RPC_REQUEST;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return true;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
new file mode 100644
index 0000000..5ee764a
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceRpcResponseMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcResponseMsg implements FromDeviceMsg {
+
+    private final int requestId;
+    private final String data;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.TO_DEVICE_RPC_RESPONSE;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java
new file mode 100644
index 0000000..97ab6d9
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToDeviceSessionActorMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ToDeviceSessionActorMsg extends SessionAwareMsg, Serializable {
+
+    ToDeviceMsg getMsg();
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
new file mode 100644
index 0000000..a684681
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcRequestMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToServerRpcRequestMsg implements FromDeviceMsg {
+
+    private final int requestId;
+    private final String method;
+    private final String params;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.TO_SERVER_RPC_REQUEST;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
new file mode 100644
index 0000000..ccce825
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/ToServerRpcResponseMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import lombok.Data;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToServerRpcResponseMsg implements ToDeviceMsg {
+
+    private final int requestId;
+    private final String data;
+
+    @Override
+    public MsgType getMsgType() {
+        return MsgType.TO_SERVER_RPC_RESPONSE;
+    }
+
+    @Override
+    public boolean isSuccess() {
+        return true;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java b/common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java
new file mode 100644
index 0000000..4f5c936
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/core/UpdateAttributesRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.core;
+
+import java.util.Set;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+
+public interface UpdateAttributesRequest extends FromDeviceRequestMsg {
+
+    Set<AttributeKvEntry> getAttributes();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java
new file mode 100644
index 0000000..c559111
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/device/BasicToDeviceActorMsg.java
@@ -0,0 +1,101 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.device;
+
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ToDeviceActorSessionMsg;
+
+import java.util.Optional;
+
+@ToString
+public class BasicToDeviceActorMsg implements ToDeviceActorMsg {
+
+    private static final long serialVersionUID = -1866795134993115408L;
+
+    private final TenantId tenantId;
+    private final CustomerId customerId;
+    private final DeviceId deviceId;
+    private final SessionId sessionId;
+    private final SessionType sessionType;
+    private final ServerAddress serverAddress;
+    private final FromDeviceMsg msg;
+
+    public BasicToDeviceActorMsg(ToDeviceActorMsg other, FromDeviceMsg msg) {
+        this(null, other.getTenantId(), other.getCustomerId(), other.getDeviceId(), other.getSessionId(), other.getSessionType(), msg);
+    }
+
+    public BasicToDeviceActorMsg(ToDeviceActorSessionMsg msg, SessionType sessionType) {
+        this(null, msg.getTenantId(), msg.getCustomerId(), msg.getDeviceId(), msg.getSessionId(), sessionType, msg.getSessionMsg().getMsg());
+    }
+
+    private BasicToDeviceActorMsg(ServerAddress serverAddress, TenantId tenantId, CustomerId customerId, DeviceId deviceId, SessionId sessionId, SessionType sessionType,
+                                  FromDeviceMsg msg) {
+        super();
+        this.serverAddress = serverAddress;
+        this.tenantId = tenantId;
+        this.customerId = customerId;
+        this.deviceId = deviceId;
+        this.sessionId = sessionId;
+        this.sessionType = sessionType;
+        this.msg = msg;
+    }
+
+    @Override
+    public DeviceId getDeviceId() {
+        return deviceId;
+    }
+
+    @Override
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return sessionId;
+    }
+
+    @Override
+    public SessionType getSessionType() {
+        return sessionType;
+    }
+
+    @Override
+    public Optional<ServerAddress> getServerAddress() {
+        return Optional.ofNullable(serverAddress);
+    }
+
+    @Override
+    public FromDeviceMsg getPayload() {
+        return msg;
+    }
+
+    @Override
+    public ToDeviceActorMsg toOtherAddress(ServerAddress otherAddress) {
+        return new BasicToDeviceActorMsg(otherAddress, tenantId, customerId, deviceId, sessionId, sessionType, msg);
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java
new file mode 100644
index 0000000..c02ac03
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/device/ToDeviceActorMsg.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.device;
+
+import java.io.Serializable;
+import java.util.Optional;
+
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.aware.CustomerAwareMsg;
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+
+public interface ToDeviceActorMsg extends DeviceAwareMsg, CustomerAwareMsg, TenantAwareMsg, Serializable {
+
+    SessionId getSessionId();
+
+    SessionType getSessionType();
+
+    Optional<ServerAddress> getServerAddress();
+    
+    FromDeviceMsg getPayload();
+
+    ToDeviceActorMsg toOtherAddress(ServerAddress otherAddress);
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java
new file mode 100644
index 0000000..73b8f00
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/AttributesKVMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.kv;
+
+import java.io.Serializable;
+import java.util.List;
+
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+
+public interface AttributesKVMsg extends Serializable {
+
+    List<AttributeKvEntry> getClientAttributes();
+    List<AttributeKvEntry> getSharedAttributes();
+    List<AttributeKey> getDeletedAttributes();
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java
new file mode 100644
index 0000000..32cdcaf
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/kv/BasicAttributeKVMsg.java
@@ -0,0 +1,52 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.kv;
+
+import lombok.AccessLevel;
+import lombok.Data;
+import lombok.RequiredArgsConstructor;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+
+import java.util.Collections;
+import java.util.List;
+
+@Data
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public class BasicAttributeKVMsg implements AttributesKVMsg {
+
+    private static final long serialVersionUID = 1L;
+
+    private final List<AttributeKvEntry> clientAttributes;
+    private final List<AttributeKvEntry> sharedAttributes;
+    private final List<AttributeKey> deletedAttributes;
+
+    public static BasicAttributeKVMsg fromClient(List<AttributeKvEntry> attributes) {
+        return new BasicAttributeKVMsg(attributes, Collections.emptyList(), Collections.emptyList());
+    }
+
+    public static BasicAttributeKVMsg fromShared(List<AttributeKvEntry> attributes) {
+        return new BasicAttributeKVMsg(Collections.emptyList(), attributes, Collections.emptyList());
+    }
+
+    public static BasicAttributeKVMsg from(List<AttributeKvEntry> client, List<AttributeKvEntry> shared) {
+        return new BasicAttributeKVMsg(client, shared, Collections.emptyList());
+    }
+
+    public static AttributesKVMsg fromDeleted(List<AttributeKey> shared) {
+        return new BasicAttributeKVMsg(Collections.emptyList(), Collections.emptyList(), shared);
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
new file mode 100644
index 0000000..fb56893
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.plugin;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleState;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg {
+    @Getter
+    private final TenantId tenantId;
+    private final PluginId pluginId;
+    private final RuleId ruleId;
+    @Getter
+    private final ComponentLifecycleEvent event;
+
+    public static ComponentLifecycleMsg forPlugin(TenantId tenantId, PluginId pluginId, ComponentLifecycleEvent event) {
+        return new ComponentLifecycleMsg(tenantId, pluginId, null, event);
+    }
+
+    public static ComponentLifecycleMsg forRule(TenantId tenantId, RuleId ruleId, ComponentLifecycleEvent event) {
+        return new ComponentLifecycleMsg(tenantId, null, ruleId, event);
+    }
+
+    private ComponentLifecycleMsg(TenantId tenantId, PluginId pluginId, RuleId ruleId, ComponentLifecycleEvent event) {
+        this.tenantId = tenantId;
+        this.pluginId = pluginId;
+        this.ruleId = ruleId;
+        this.event = event;
+    }
+
+    public Optional<PluginId> getPluginId() {
+        return Optional.ofNullable(pluginId);
+    }
+
+    public Optional<RuleId> getRuleId() {
+        return Optional.ofNullable(ruleId);
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java
new file mode 100644
index 0000000..1a93c25
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/RuleMsg.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg;
+
+import org.thingsboard.server.common.data.rule.Scope;
+import org.thingsboard.server.common.data.rule.RuleType;
+import org.thingsboard.server.common.msg.aware.RuleAwareMsg;
+
+/**
+ * Message that is used to deliver some data to the rule instance. 
+ * For example: aggregated statistics or command decoded from http request. 
+ * 
+ * @author ashvayka
+ *
+ * @param <V> - payload 
+ */
+public interface RuleMsg<V> extends RuleAwareMsg {
+
+	Scope getRuleLevel();
+	
+	RuleType getRuleType();
+	
+	V getPayload();
+	
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java
new file mode 100644
index 0000000..a005f1c
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/AdaptorToSessionActorMsg.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+public interface AdaptorToSessionActorMsg extends SessionMsg {
+
+    FromDeviceMsg getMsg();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java
new file mode 100644
index 0000000..11caf25
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicAdaptorToSessionActorMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+public class BasicAdaptorToSessionActorMsg extends BasicSessionMsg implements AdaptorToSessionActorMsg {
+
+    private final FromDeviceMsg msg;
+
+    public BasicAdaptorToSessionActorMsg(SessionContext ctx, FromDeviceMsg msg) {
+        super(ctx);
+        this.msg = msg;
+    }
+
+    @Override
+    public FromDeviceMsg getMsg() {
+        return msg;
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java
new file mode 100644
index 0000000..3b7d9fd
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionActorToAdaptorMsg.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import java.util.Optional;
+
+public class BasicSessionActorToAdaptorMsg extends BasicSessionMsg implements SessionActorToAdaptorMsg {
+
+    private final ToDeviceMsg msg;
+
+    public BasicSessionActorToAdaptorMsg(SessionContext ctx, ToDeviceMsg msg) {
+        super(ctx);
+        this.msg = msg;
+    }
+
+    @Override
+    public ToDeviceMsg getMsg() {
+        return msg;
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java
new file mode 100644
index 0000000..7bcc55e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicSessionMsg.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public class BasicSessionMsg implements SessionMsg {
+
+    private final SessionContext ctx;
+
+    public BasicSessionMsg(SessionContext ctx) {
+        super();
+        this.ctx = ctx;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return ctx.getSessionId();
+    }
+
+    @Override
+    public SessionContext getSessionContext() {
+        return ctx;
+    }
+
+    @Override
+    public String toString() {
+        return "BasicSessionMsg [ctx=" + ctx + "]";
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java
new file mode 100644
index 0000000..ced4843
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/BasicToDeviceActorSessionMsg.java
@@ -0,0 +1,76 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class BasicToDeviceActorSessionMsg implements ToDeviceActorSessionMsg {
+
+    private final TenantId tenantId;
+    private final CustomerId customerId;
+    private final DeviceId deviceId;
+    private final AdaptorToSessionActorMsg msg;
+
+    public BasicToDeviceActorSessionMsg(Device device, AdaptorToSessionActorMsg msg) {
+        super();
+        this.tenantId = device.getTenantId();
+        this.customerId = device.getCustomerId();
+        this.deviceId = device.getId();
+        this.msg = msg;
+    }
+
+    public BasicToDeviceActorSessionMsg(ToDeviceActorSessionMsg deviceMsg) {
+        this.tenantId = deviceMsg.getTenantId();
+        this.customerId = deviceMsg.getCustomerId();
+        this.deviceId = deviceMsg.getDeviceId();
+        this.msg = deviceMsg.getSessionMsg();
+    }
+
+    @Override
+    public DeviceId getDeviceId() {
+        return deviceId;
+    }
+
+    @Override
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return msg.getSessionId();
+    }
+
+    @Override
+    public AdaptorToSessionActorMsg getSessionMsg() {
+        return msg;
+    }
+
+    @Override
+    public String toString() {
+        return "BasicToDeviceActorSessionMsg [tenantId=" + tenantId + ", customerId=" + customerId + ", deviceId=" + deviceId + ", msg=" + msg
+                + "]";
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
new file mode 100644
index 0000000..d188527
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ctrl/SessionCloseMsg.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session.ctrl;
+
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+
+public class SessionCloseMsg implements SessionCtrlMsg {
+
+    private final SessionId sessionId;
+    private final boolean timeout;
+
+    public SessionCloseMsg(SessionId sessionId, boolean timeout) {
+        super();
+        this.sessionId = sessionId;
+        this.timeout = timeout;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return sessionId;
+    }
+
+    public boolean isTimeout() {
+        return timeout;
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java
new file mode 100644
index 0000000..7323a2d
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/ProcessingTimeoutException.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session.ex;
+
+public class ProcessingTimeoutException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java
new file mode 100644
index 0000000..9d22cca
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionAuthException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session.ex;
+
+public class SessionAuthException extends SessionException {
+
+    private static final long serialVersionUID = 1L;
+    
+    public SessionAuthException(String msg) {
+        super(msg);
+    }
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java
new file mode 100644
index 0000000..444c364
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ex/SessionException.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session.ex;
+
+public class SessionException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public SessionException(String msg){
+        super(msg);
+    }
+    
+    public SessionException(Exception cause){
+        super(cause);
+    }
+
+    public SessionException(String msg, Exception cause){
+        super(msg, cause);
+    }
+
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
new file mode 100644
index 0000000..76da405
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FeatureType.java
@@ -0,0 +1,20 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+public enum FeatureType {
+    ATTRIBUTES, TELEMETRY, RPC
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
new file mode 100644
index 0000000..b717fec
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import java.io.Serializable;
+
+public interface FromDeviceMsg extends Serializable {
+
+    MsgType getMsgType();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java
new file mode 100644
index 0000000..7709462
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/FromDeviceRequestMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface FromDeviceRequestMsg extends FromDeviceMsg {
+
+    Integer getRequestId();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
new file mode 100644
index 0000000..1b91425
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/MsgType.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+public enum MsgType {
+    GET_ATTRIBUTES_REQUEST(true), POST_ATTRIBUTES_REQUEST(true), GET_ATTRIBUTES_RESPONSE,
+    SUBSCRIBE_ATTRIBUTES_REQUEST, UNSUBSCRIBE_ATTRIBUTES_REQUEST, ATTRIBUTES_UPDATE_NOTIFICATION,
+
+    POST_TELEMETRY_REQUEST(true), STATUS_CODE_RESPONSE,
+
+    SUBSCRIBE_RPC_COMMANDS_REQUEST, UNSUBSCRIBE_RPC_COMMANDS_REQUEST,
+    TO_DEVICE_RPC_REQUEST, TO_DEVICE_RPC_RESPONSE, TO_DEVICE_RPC_RESPONSE_ACK,
+
+    TO_SERVER_RPC_REQUEST(true), TO_SERVER_RPC_RESPONSE,
+
+    RULE_ENGINE_ERROR,
+
+    SESSION_CLOSE;
+
+    private final boolean requiresRulesProcessing;
+
+    MsgType() {
+        this(false);
+    }
+
+    MsgType(boolean requiresRulesProcessing) {
+        this.requiresRulesProcessing = requiresRulesProcessing;
+    }
+
+    public boolean requiresRulesProcessing() {
+        return requiresRulesProcessing;
+    }
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java
new file mode 100644
index 0000000..f85545e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionActorToAdaptorMsg.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+public interface SessionActorToAdaptorMsg extends SessionMsg {
+
+    ToDeviceMsg getMsg();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java
new file mode 100644
index 0000000..d437dda
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionContext.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+
+public interface SessionContext extends SessionAwareMsg {
+
+    SessionType getSessionType();
+
+    void onMsg(SessionActorToAdaptorMsg msg) throws SessionException;
+
+    void onMsg(SessionCtrlMsg msg) throws SessionException;
+
+    void onError(SessionException e);
+
+    boolean isClosed();
+
+    long getTimeout();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
new file mode 100644
index 0000000..68c1d8d
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionCtrlMsg.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+public interface SessionCtrlMsg extends SessionAwareMsg {
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsg.java
new file mode 100644
index 0000000..f4a51c5
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionMsg.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+public interface SessionMsg extends SessionAwareMsg {
+
+    SessionContext getSessionContext();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionType.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionType.java
new file mode 100644
index 0000000..1e432f9
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/SessionType.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+public enum SessionType {
+
+    SYNC, ASYNC;
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java
new file mode 100644
index 0000000..aa3650e
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceActorSessionMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import org.thingsboard.server.common.msg.aware.CustomerAwareMsg;
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+public interface ToDeviceActorSessionMsg extends DeviceAwareMsg, CustomerAwareMsg, TenantAwareMsg, SessionAwareMsg {
+
+    AdaptorToSessionActorMsg getSessionMsg();
+
+}
diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
new file mode 100644
index 0000000..8c87f60
--- /dev/null
+++ b/common/message/src/main/java/org/thingsboard/server/common/msg/session/ToDeviceMsg.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.msg.session;
+
+import java.io.Serializable;
+
+public interface ToDeviceMsg extends Serializable {
+
+    boolean isSuccess();
+
+    MsgType getMsgType();
+
+}

common/pom.xml 43(+43 -0)

diff --git a/common/pom.xml b/common/pom.xml
new file mode 100644
index 0000000..97ff8ab
--- /dev/null
+++ b/common/pom.xml
@@ -0,0 +1,43 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>server</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server</groupId>
+    <artifactId>common</artifactId>
+    <packaging>pom</packaging>
+
+    <name>Thingsboard Server Commons</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/..</main.dir>
+    </properties>
+    <modules>
+        <module>data</module>
+        <module>message</module>
+	<module>transport</module>
+    </modules>
+
+</project>
diff --git a/common/transport/pom.xml b/common/transport/pom.xml
new file mode 100644
index 0000000..e57d3d7
--- /dev/null
+++ b/common/transport/pom.xml
@@ -0,0 +1,79 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard.server</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>common</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server.common</groupId>
+    <artifactId>transport</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Common Transport components</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>data</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>message</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java
new file mode 100644
index 0000000..2c826ec
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/AdaptorException.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.adaptor;
+
+public class AdaptorException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public AdaptorException() {
+        super();
+    }
+
+    public AdaptorException(String cause) {
+        super(cause);
+    }
+
+    public AdaptorException(Exception cause) {
+        super(cause);
+    }
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
new file mode 100644
index 0000000..27c7052
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/adaptor/JsonConverter.java
@@ -0,0 +1,199 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.adaptor;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import com.google.gson.*;
+import org.thingsboard.server.common.msg.core.*;
+
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+
+public class JsonConverter {
+
+    private static final Gson GSON = new Gson();
+
+    public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject) throws JsonSyntaxException {
+        return convertToTelemetry(jsonObject, BasicRequest.DEFAULT_REQUEST_ID);
+    }
+
+    public static TelemetryUploadRequest convertToTelemetry(JsonElement jsonObject, int requestId) throws JsonSyntaxException {
+        BasicTelemetryUploadRequest request = new BasicTelemetryUploadRequest(requestId);
+        long systemTs = System.currentTimeMillis();
+        if (jsonObject.isJsonObject()) {
+            parseObject(request, systemTs, jsonObject);
+        } else if (jsonObject.isJsonArray()) {
+            jsonObject.getAsJsonArray().forEach(je -> {
+                if (je.isJsonObject()) {
+                    parseObject(request, systemTs, je.getAsJsonObject());
+                } else {
+                    throw new JsonSyntaxException("Can't parse value: " + je);
+                }
+            });
+        } else {
+            throw new JsonSyntaxException("Can't parse value: " + jsonObject);
+        }
+        return request;
+    }
+
+    public static ToServerRpcRequestMsg convertToServerRpcRequest(JsonElement json, int requestId) throws JsonSyntaxException {
+        JsonObject object = json.getAsJsonObject();
+        return new ToServerRpcRequestMsg(requestId, object.get("method").getAsString(), GSON.toJson(object.get("params")));
+    }
+
+    private static void parseObject(BasicTelemetryUploadRequest request, long systemTs, JsonElement jsonObject) {
+        JsonObject jo = jsonObject.getAsJsonObject();
+        if (jo.has("ts") && jo.has("values")) {
+            parseWithTs(request, jo);
+        } else {
+            parseWithoutTs(request, systemTs, jo);
+        }
+    }
+
+    private static void parseWithoutTs(BasicTelemetryUploadRequest request, long systemTs, JsonObject jo) {
+        for (KvEntry entry : parseValues(jo)) {
+            request.add(systemTs, entry);
+        }
+    }
+
+    private static void parseWithTs(BasicTelemetryUploadRequest request, JsonObject jo) {
+        long ts = jo.get("ts").getAsLong();
+        JsonObject valuesObject = jo.get("values").getAsJsonObject();
+        for (KvEntry entry : parseValues(valuesObject)) {
+            request.add(ts, entry);
+        }
+    }
+
+    private static List<KvEntry> parseValues(JsonObject valuesObject) {
+        List<KvEntry> result = new ArrayList<>();
+        for (Entry<String, JsonElement> valueEntry : valuesObject.entrySet()) {
+            JsonElement element = valueEntry.getValue();
+            if (element.isJsonPrimitive()) {
+                JsonPrimitive value = element.getAsJsonPrimitive();
+                if (value.isString()) {
+                    result.add(new StringDataEntry(valueEntry.getKey(), value.getAsString()));
+                } else if (value.isBoolean()) {
+                    result.add(new BooleanDataEntry(valueEntry.getKey(), value.getAsBoolean()));
+                } else if (value.isNumber()) {
+                    if (value.getAsString().contains(".")) {
+                        result.add(new DoubleDataEntry(valueEntry.getKey(), value.getAsDouble()));
+                    } else {
+                        result.add(new LongDataEntry(valueEntry.getKey(), value.getAsLong()));
+                    }
+                } else {
+                    throw new JsonSyntaxException("Can't parse value: " + value);
+                }
+            } else {
+                throw new JsonSyntaxException("Can't parse value: " + element);
+            }
+        }
+        return result;
+    }
+
+    public static UpdateAttributesRequest convertToAttributes(JsonElement element) {
+        return convertToAttributes(element, BasicRequest.DEFAULT_REQUEST_ID);
+    }
+
+    public static UpdateAttributesRequest convertToAttributes(JsonElement element, int requestId) {
+        if (element.isJsonObject()) {
+            BasicUpdateAttributesRequest request = new BasicUpdateAttributesRequest(requestId);
+            long ts = System.currentTimeMillis();
+            request.add(parseValues(element.getAsJsonObject()).stream().map(kv -> new BaseAttributeKvEntry(kv, ts)).collect(Collectors.toList()));
+            return request;
+        } else {
+            throw new JsonSyntaxException("Can't parse value: " + element);
+        }
+    }
+
+    public static JsonObject toJson(AttributesKVMsg payload, boolean asMap) {
+        JsonObject result = new JsonObject();
+        if (asMap) {
+            if (!payload.getClientAttributes().isEmpty()) {
+                JsonObject attrObject = new JsonObject();
+                payload.getClientAttributes().forEach(addToObject(attrObject));
+                result.add("client", attrObject);
+            }
+            if (!payload.getSharedAttributes().isEmpty()) {
+                JsonObject attrObject = new JsonObject();
+                payload.getSharedAttributes().forEach(addToObject(attrObject));
+                result.add("shared", attrObject);
+            }
+        } else {
+            payload.getClientAttributes().forEach(addToObject(result));
+            payload.getSharedAttributes().forEach(addToObject(result));
+        }
+        if (!payload.getDeletedAttributes().isEmpty()) {
+            JsonArray attrObject = new JsonArray();
+            payload.getDeletedAttributes().forEach(addToObject(attrObject));
+            result.add("deleted", attrObject);
+        }
+        return result;
+    }
+
+    private static Consumer<AttributeKey> addToObject(JsonArray result) {
+        return key -> {
+            result.add(key.getAttributeKey());
+        };
+    }
+
+    private static Consumer<AttributeKvEntry> addToObject(JsonObject result) {
+        return de -> {
+            JsonPrimitive value;
+            switch (de.getDataType()) {
+                case BOOLEAN:
+                    value = new JsonPrimitive(de.getBooleanValue().get());
+                    break;
+                case DOUBLE:
+                    value = new JsonPrimitive(de.getDoubleValue().get());
+                    break;
+                case LONG:
+                    value = new JsonPrimitive(de.getLongValue().get());
+                    break;
+                case STRING:
+                    value = new JsonPrimitive(de.getStrValue().get());
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unsupported data type: " + de.getDataType());
+            }
+            result.add(de.getKey(), value);
+        };
+    }
+
+    public static JsonObject toJson(ToDeviceRpcRequestMsg msg, boolean includeRequestId) {
+        JsonObject result = new JsonObject();
+        if (includeRequestId) {
+            result.addProperty("id", msg.getRequestId());
+        }
+        result.addProperty("method", msg.getMethod());
+        result.add("params", new JsonParser().parse(msg.getParams()));
+        return result;
+    }
+
+    public static JsonElement toJson(ToServerRpcResponseMsg msg) {
+        return new JsonParser().parse(msg.getData());
+    }
+
+    public static JsonElement toErrorJson(String errorMsg) {
+        JsonObject error = new JsonObject();
+        error.addProperty("error", errorMsg);
+        return error;
+    }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java
new file mode 100644
index 0000000..40d4be9
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthResult.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.auth;
+
+import org.thingsboard.server.common.data.id.DeviceId;
+
+public class DeviceAuthResult {
+
+    private final boolean success;
+    private final DeviceId deviceId;
+    private final String errorMsg;
+
+    public static DeviceAuthResult of(DeviceId deviceId) {
+        return new DeviceAuthResult(true, deviceId, null);
+    }
+
+    public static DeviceAuthResult of(String errorMsg) {
+        return new DeviceAuthResult(false, null, errorMsg);
+    }
+
+    private DeviceAuthResult(boolean success, DeviceId deviceId, String errorMsg) {
+        super();
+        this.success = success;
+        this.deviceId = deviceId;
+        this.errorMsg = errorMsg;
+    }
+
+    public boolean isSuccess() {
+        return success;
+    }
+
+    public DeviceId getDeviceId() {
+        return deviceId;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    @Override
+    public String toString() {
+        return "DeviceAuthResult [success=" + success + ", deviceId=" + deviceId + ", errorMsg=" + errorMsg + "]";
+    }
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java
new file mode 100644
index 0000000..67cdb51
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/auth/DeviceAuthService.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.auth;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+
+import java.util.Optional;
+
+public interface DeviceAuthService {
+
+    DeviceAuthResult process(DeviceCredentialsFilter credentials);
+
+    Optional<Device> findDeviceById(DeviceId deviceId);
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
new file mode 100644
index 0000000..b762ed9
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport.session;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public abstract class DeviceAwareSessionContext implements SessionContext {
+
+    protected final DeviceAuthService authService;
+    protected final SessionMsgProcessor processor;
+
+    protected volatile Device device;
+
+    public DeviceAwareSessionContext(SessionMsgProcessor processor, DeviceAuthService authService) {
+        this.processor = processor;
+        this.authService = authService;
+    }
+
+    public boolean login(DeviceCredentialsFilter credentials) {
+        DeviceAuthResult result = authService.process(credentials);
+        if (result.isSuccess()) {
+            Optional<Device> deviceOpt = authService.findDeviceById(result.getDeviceId());
+            if (deviceOpt.isPresent()) {
+                device = deviceOpt.get();
+            }
+            return true;
+        } else {
+            log.debug("Can't find device using credentials [{}] due to {}", credentials, result.getErrorMsg());
+            return false;
+        }
+    }
+
+    public Device getDevice() {
+        return device;
+    }
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java
new file mode 100644
index 0000000..a9760aa
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/SessionMsgProcessor.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport;
+
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+
+public interface SessionMsgProcessor {
+
+    void process(SessionAwareMsg msg);
+
+}
diff --git a/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
new file mode 100644
index 0000000..c2b1fe0
--- /dev/null
+++ b/common/transport/src/main/java/org/thingsboard/server/common/transport/TransportAdaptor.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.common.transport;
+
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+
+import java.util.Optional;
+
+public interface TransportAdaptor<C extends SessionContext, T, V> {
+
+    AdaptorToSessionActorMsg convertToActorMsg(C ctx, MsgType type, T inbound) throws AdaptorException;
+
+    Optional<V> convertToAdaptorMsg(C ctx, SessionActorToAdaptorMsg msg) throws AdaptorException;
+
+}
diff --git a/extensions-api/pom.xml b/extensions-api/pom.xml
new file mode 100644
index 0000000..4af3de3
--- /dev/null
+++ b/extensions-api/pom.xml
@@ -0,0 +1,95 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>server</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server</groupId>
+    <artifactId>extensions-api</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Extensions API</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>data</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>message</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+
+</project>
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Action.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Action.java
new file mode 100644
index 0000000..6dd4327
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Action.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Action {
+
+    String name();
+
+    ComponentScope scope() default ComponentScope.TENANT;
+
+    String descriptor() default "EmptyJsonDescriptor.json";
+
+    Class<?> configuration() default EmptyComponentConfiguration.class;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java
new file mode 100644
index 0000000..79f728f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/ConfigurableComponent.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.component;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ConfigurableComponent<T> {
+
+    void init(T configuration);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java
new file mode 100644
index 0000000..f99272e
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/EmptyComponentConfiguration.java
@@ -0,0 +1,22 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.component;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class EmptyComponentConfiguration {
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Filter.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Filter.java
new file mode 100644
index 0000000..6c20ae9
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Filter.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Filter {
+
+    String name();
+
+    ComponentScope scope() default ComponentScope.TENANT;
+
+    String descriptor() default "EmptyJsonDescriptor.json";
+
+    Class<?> configuration() default EmptyComponentConfiguration.class;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Plugin.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Plugin.java
new file mode 100644
index 0000000..5c4c632
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Plugin.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Plugin {
+
+    String name();
+
+    ComponentScope scope() default ComponentScope.TENANT;
+
+    String descriptor() default "EmptyJsonDescriptor.json";
+
+    Class<?> configuration() default EmptyComponentConfiguration.class;
+
+    Class<?>[] actions();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java
new file mode 100644
index 0000000..d6b1bce
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/component/Processor.java
@@ -0,0 +1,40 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.component;
+
+import org.thingsboard.server.common.data.plugin.ComponentScope;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface Processor {
+
+    String name();
+
+    ComponentScope scope() default ComponentScope.TENANT;
+
+    String descriptor() default "EmptyJsonDescriptor.json";
+
+    Class<?> configuration() default EmptyComponentConfiguration.class;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java
new file mode 100644
index 0000000..242becc
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configurable.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.configuration;
+
+public interface Configurable<C extends Configuration> {
+
+    Class<C> getConfigurationClass();
+
+    void validate(C configuration) throws ConfigurationValidationException;
+
+    void configure(C configuration);
+    
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java
new file mode 100644
index 0000000..3c2ba30
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/Configuration.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.configuration;
+
+import java.io.Serializable;
+
+public interface Configuration extends Serializable {
+
+    // TODO: Figure out how to define and visualize configurations
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java
new file mode 100644
index 0000000..416668c
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/configuration/ConfigurationValidationException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.configuration;
+
+public class ConfigurationValidationException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public ConfigurationValidationException(String msg, Exception cause){
+        super(msg, cause);
+    }
+    
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java
new file mode 100644
index 0000000..4f2d760
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributes.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.device;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+
+import java.util.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DeviceAttributes {
+
+    private final Map<String, AttributeKvEntry> clientSideAttributesMap;
+    private final Map<String, AttributeKvEntry> serverPrivateAttributesMap;
+    private final Map<String, AttributeKvEntry> serverPublicAttributesMap;
+
+    public DeviceAttributes(List<AttributeKvEntry> clientSideAttributes, List<AttributeKvEntry> serverPrivateAttributes, List<AttributeKvEntry> serverPublicAttributes) {
+        this.clientSideAttributesMap = mapAttributes(clientSideAttributes);
+        this.serverPrivateAttributesMap = mapAttributes(serverPrivateAttributes);
+        this.serverPublicAttributesMap = mapAttributes(serverPublicAttributes);
+    }
+
+    private static Map<String, AttributeKvEntry> mapAttributes(List<AttributeKvEntry> attributes) {
+        Map<String, AttributeKvEntry> result = new HashMap<>();
+        for (AttributeKvEntry attribute : attributes) {
+            result.put(attribute.getKey(), attribute);
+        }
+        return result;
+    }
+
+    public Collection<AttributeKvEntry> getClientSideAttributes() {
+        return clientSideAttributesMap.values();
+    }
+
+    public Collection<AttributeKvEntry> getServerSideAttributes() {
+        return serverPrivateAttributesMap.values();
+    }
+
+    public Collection<AttributeKvEntry> getServerSidePublicAttributes() {
+        return serverPublicAttributesMap.values();
+    }
+
+    public Optional<AttributeKvEntry> getClientSideAttribute(String attribute) {
+        return Optional.ofNullable(clientSideAttributesMap.get(attribute));
+    }
+
+    public Optional<AttributeKvEntry> getServerPrivateAttribute(String attribute) {
+        return Optional.ofNullable(serverPrivateAttributesMap.get(attribute));
+    }
+
+    public Optional<AttributeKvEntry> getServerPublicAttribute(String attribute) {
+        return Optional.ofNullable(serverPublicAttributesMap.get(attribute));
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java
new file mode 100644
index 0000000..ebeb58f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/DeviceAttributesEventNotificationMsg.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.device;
+
+import lombok.Getter;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKey;
+
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+public class DeviceAttributesEventNotificationMsg implements ToDeviceActorNotificationMsg {
+
+    @Getter private final TenantId tenantId;
+    @Getter private final DeviceId deviceId;
+    @Getter private final Set<AttributeKey> keys;
+    @Getter private final boolean deleted;
+
+    public static DeviceAttributesEventNotificationMsg onUpdate(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
+        return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, false);
+    }
+
+    public static DeviceAttributesEventNotificationMsg onDelete(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys) {
+        return new DeviceAttributesEventNotificationMsg(tenantId, deviceId, keys, true);
+    }
+
+    private DeviceAttributesEventNotificationMsg(TenantId tenantId, DeviceId deviceId, Set<AttributeKey> keys, boolean deleted) {
+        this.tenantId = tenantId;
+        this.deviceId = deviceId;
+        this.keys = keys;
+        this.deleted = deleted;
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java
new file mode 100644
index 0000000..2236f14
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/device/ToDeviceActorNotificationMsg.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.device;
+
+import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface ToDeviceActorNotificationMsg extends TenantAwareMsg, DeviceAwareMsg, Serializable {
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java
new file mode 100644
index 0000000..c20b91d
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/AbstractPlugin.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.handlers.*;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public abstract class AbstractPlugin<T> implements Plugin<T> {
+
+    @Override
+    public void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg) {
+        getWebsocketMsgHandler().process(ctx, wsMsg);
+    }
+
+    @Override
+    public void process(PluginContext ctx, PluginRestMsg msg) {
+        getRestMsgHandler().process(ctx, msg);
+    }
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        getRuleMsgHandler().process(ctx, tenantId, ruleId, msg);
+    }
+
+    @Override
+    public void process(PluginContext ctx, RpcMsg msg) {
+        getRpcMsgHandler().process(ctx, msg);
+    }
+
+    @Override
+    public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+        throw new IllegalStateException("Device RPC messages is not handled in current plugin!");
+    }
+
+    @Override
+    public void process(PluginContext ctx, TimeoutMsg<?> msg) {
+        throw new IllegalStateException("Timeouts is not handled in current plugin!");
+    }
+
+    @Override
+    public void onServerAdded(PluginContext ctx, ServerAddress server) {
+    }
+
+    @Override
+    public void onServerRemoved(PluginContext ctx, ServerAddress server) {
+    }
+
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return new DefaultRuleMsgHandler();
+    }
+
+    protected RestMsgHandler getRestMsgHandler() {
+        return new DefaultRestMsgHandler();
+    }
+
+    protected WebsocketMsgHandler getWebsocketMsgHandler() {
+        return new DefaultWebsocketMsgHandler();
+    }
+
+    protected RpcMsgHandler getRpcMsgHandler() {
+        return new DefaultRpcMsgHandler();
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java
new file mode 100644
index 0000000..c9b27f7
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRestMsgHandler.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpMethod;
+import org.springframework.web.HttpRequestMethodNotSupportedException;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+
+import javax.servlet.ServletException;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DefaultRestMsgHandler implements RestMsgHandler {
+
+    protected final ObjectMapper jsonMapper = new ObjectMapper();
+
+    @Override
+    public void process(PluginContext ctx, PluginRestMsg msg) {
+        try {
+            log.debug("[{}] Processing REST msg: {}", ctx.getPluginId(), msg);
+            HttpMethod method = msg.getRequest().getMethod();
+            switch (method) {
+                case GET:
+                    handleHttpGetRequest(ctx, msg);
+                    break;
+                case POST:
+                    handleHttpPostRequest(ctx, msg);
+                    break;
+                case DELETE:
+                    handleHttpDeleteRequest(ctx, msg);
+                    break;
+                default:
+                    msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(method.name()));
+            }
+            log.debug("[{}] Processed REST msg.", ctx.getPluginId());
+        } catch (Exception e) {
+            log.warn("[{}] Exception during REST msg processing: {}", ctx.getPluginId(), e.getMessage(), e);
+            msg.getResponseHolder().setErrorResult(e);
+        }
+    }
+
+    protected void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(HttpMethod.GET.name()));
+    }
+
+    protected void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(HttpMethod.POST.name()));
+    }
+
+    protected void handleHttpDeleteRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        msg.getResponseHolder().setErrorResult(new HttpRequestMethodNotSupportedException(HttpMethod.DELETE.name()));
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java
new file mode 100644
index 0000000..c428cdb
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRpcMsgHandler.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class DefaultRpcMsgHandler implements RpcMsgHandler {
+
+    @Override
+    public void process(PluginContext ctx, RpcMsg msg) {
+        throw new RuntimeException("Not registered msg type: " + msg.getMsgClazz() + "!");
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java
new file mode 100644
index 0000000..6550f87
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultRuleMsgHandler.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.msg.GetAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TelemetryUploadRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.UpdateAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DefaultRuleMsgHandler implements RuleMsgHandler {
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        if (msg instanceof TelemetryUploadRequestRuleToPluginMsg) {
+            handleTelemetryUploadRequest(ctx, tenantId, ruleId, (TelemetryUploadRequestRuleToPluginMsg) msg);
+        } else if (msg instanceof UpdateAttributesRequestRuleToPluginMsg) {
+            handleUpdateAttributesRequest(ctx, tenantId, ruleId, (UpdateAttributesRequestRuleToPluginMsg) msg);
+        } else if (msg instanceof GetAttributesRequestRuleToPluginMsg) {
+            handleGetAttributesRequest(ctx, tenantId, ruleId, (GetAttributesRequestRuleToPluginMsg) msg);
+        }
+        //TODO: handle subscriptions to attribute updates.
+    }
+
+    protected void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) {
+        msgTypeNotSupported(msg.getPayload().getMsgType());
+    }
+
+    protected void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) {
+        msgTypeNotSupported(msg.getPayload().getMsgType());
+    }
+
+    protected void handleTelemetryUploadRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, TelemetryUploadRequestRuleToPluginMsg msg) {
+        msgTypeNotSupported(msg.getPayload().getMsgType());
+    }
+
+    private void msgTypeNotSupported(MsgType msgType) {
+        throw new RuntimeException("Not supported msg type: " + msgType + "!");
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
new file mode 100644
index 0000000..fab11bb
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/DefaultWebsocketMsgHandler.java
@@ -0,0 +1,103 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+import org.thingsboard.server.extensions.api.plugins.ws.WsSessionMetaData;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.*;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DefaultWebsocketMsgHandler implements WebsocketMsgHandler {
+
+    protected final ObjectMapper jsonMapper = new ObjectMapper();
+
+    protected final Map<String, WsSessionMetaData> wsSessionsMap = new HashMap<>();
+
+    @Override
+    public void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg) {
+        PluginWebsocketSessionRef sessionRef = wsMsg.getSessionRef();
+        if (log.isTraceEnabled()) {
+            log.trace("[{}] Processing: {}", sessionRef.getSessionId(), wsMsg);
+        } else {
+            log.debug("[{}] Processing: {}", sessionRef.getSessionId(), wsMsg.getClass().getSimpleName());
+        }
+        if (wsMsg instanceof SessionEventPluginWebSocketMsg) {
+            handleWebSocketSessionEvent(ctx, sessionRef, (SessionEventPluginWebSocketMsg) wsMsg);
+        } else if (wsMsg instanceof TextPluginWebSocketMsg || wsMsg instanceof BinaryPluginWebSocketMsg) {
+            handleWebSocketMsg(ctx, sessionRef, wsMsg);
+        } else if (wsMsg instanceof PongPluginWebsocketMsg) {
+            handleWebSocketPongEvent(ctx, sessionRef);
+        }
+    }
+
+    protected void handleWebSocketMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, PluginWebsocketMsg<?> wsMsg) {
+        throw new RuntimeException("Web-sockets are not supported by current plugin!");
+    }
+
+    protected void cleanupWebSocketSession(PluginContext ctx, String sessionId) {
+
+    }
+
+    protected void handleWebSocketSessionEvent(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SessionEventPluginWebSocketMsg wsMsg) {
+        String sessionId = sessionRef.getSessionId();
+        SessionEvent event = wsMsg.getPayload();
+        log.debug("[{}] Processing: {}", sessionId, event);
+        switch (event.getEventType()) {
+            case ESTABLISHED:
+                wsSessionsMap.put(sessionId, new WsSessionMetaData(sessionRef));
+                break;
+            case ERROR:
+                log.debug("[{}] Unknown websocket session error: {}. ", sessionId, event.getError().get());
+                break;
+            case CLOSED:
+                wsSessionsMap.remove(sessionId);
+                cleanupWebSocketSession(ctx, sessionId);
+                break;
+        }
+    }
+
+    protected void handleWebSocketPongEvent(PluginContext ctx, PluginWebsocketSessionRef sessionRef) {
+        String sessionId = sessionRef.getSessionId();
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD != null) {
+            log.debug("[{}] Updating session metadata: {}", sessionId, sessionRef);
+            sessionMD.setSessionRef(sessionRef);
+            sessionMD.setLastActivityTime(System.currentTimeMillis());
+        }
+    }
+
+    public void clear(PluginContext ctx) {
+        wsSessionsMap.values().stream().forEach(v -> {
+            try {
+                ctx.close(v.getSessionRef());
+            } catch (IOException e) {
+                log.debug("[{}] Failed to close session: {}", v.getSessionRef().getSessionId(), e.getMessage(), e);
+            }
+        });
+        wsSessionsMap.clear();
+    }
+}
\ No newline at end of file
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java
new file mode 100644
index 0000000..d91ba7a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RestMsgHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RestMsgHandler {
+
+    void process(PluginContext ctx, PluginRestMsg msg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java
new file mode 100644
index 0000000..42aeb81
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RpcMsgHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RpcMsgHandler {
+
+    void process(PluginContext ctx, RpcMsg msg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java
new file mode 100644
index 0000000..c356575
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/RuleMsgHandler.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleMsgHandler {
+
+    void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException;
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java
new file mode 100644
index 0000000..8526ec5
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/handlers/WebsocketMsgHandler.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.handlers;
+
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface WebsocketMsgHandler {
+
+    void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java
new file mode 100644
index 0000000..398f5a3
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractPluginToRuleMsg.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public class AbstractPluginToRuleMsg<T extends Serializable> implements PluginToRuleMsg<T> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final UUID uid;
+    private final TenantId tenantId;
+    private final RuleId ruleId;
+    private final T payload;
+
+    public AbstractPluginToRuleMsg(UUID uid, TenantId tenantId, RuleId ruleId, T payload) {
+        super();
+        this.uid = uid;
+        this.tenantId = tenantId;
+        this.ruleId = ruleId;
+        this.payload = payload;
+    }
+
+    @Override
+    public UUID getUid() {
+        return uid;
+    }
+
+    @Override
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    @Override
+    public T getPayload() {
+        return payload;
+    }
+
+    @Override
+    public RuleId getRuleId() {
+        return ruleId;
+    }
+
+
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java
new file mode 100644
index 0000000..ead9e9d
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/AbstractRuleToPluginMsg.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+public abstract class AbstractRuleToPluginMsg<T extends Serializable> implements RuleToPluginMsg<T> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final UUID uid;
+    private final TenantId tenantId;
+    private final CustomerId customerId;
+    private final DeviceId deviceId;
+    private final T payload;
+
+    public AbstractRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, T payload) {
+        super();
+        this.uid = UUID.randomUUID();
+        this.tenantId = tenantId;
+        this.customerId = customerId;
+        this.deviceId = deviceId;
+        this.payload = payload;
+    }
+
+    @Override
+    public UUID getUid() {
+        return uid;
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+    @Override
+    public DeviceId getDeviceId() {
+        return deviceId;
+    }
+
+    public T getPayload() {
+        return payload;
+    }
+
+    @Override
+    public String toString() {
+        return "AbstractRuleToPluginMsg [uid=" + uid + ", tenantId=" + tenantId + ", customerId=" + customerId
+                + ", deviceId=" + deviceId + ", payload=" + payload + "]";
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java
new file mode 100644
index 0000000..f9e1883
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/FromDeviceRpcResponse.java
@@ -0,0 +1,44 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RequiredArgsConstructor
+@ToString
+public class FromDeviceRpcResponse {
+    @Getter
+    private final UUID id;
+    private final String response;
+    private final RpcError error;
+
+    public Optional<String> getResponse() {
+        return Optional.ofNullable(response);
+    }
+
+    public Optional<RpcError> getError() {
+        return Optional.ofNullable(error);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..4c821f6
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetAttributesRequestRuleToPluginMsg.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class GetAttributesRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<GetAttributesRequest> {
+
+    private static final long serialVersionUID = 1L;
+
+    public GetAttributesRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, GetAttributesRequest payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..c824a3f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/GetRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+public class GetRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<String[]>{
+
+    private static final long serialVersionUID = 1L;
+
+    public GetRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId,
+            String[] payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java
new file mode 100644
index 0000000..a6bd6e5
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/PluginToRuleMsg.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.rules.ToRuleActorMsg;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * The basic interface for messages that are sent from particular plugin to rule
+ * instance
+ * 
+ * @author ashvayka
+ * @see RuleToPluginMsg
+ *
+ */
+public interface PluginToRuleMsg<V extends Serializable> extends ToRuleActorMsg, Serializable {
+
+    /**
+     * Returns the unique identifier of the message
+     * 
+     * @return unique identifier of the message.
+     */
+    UUID getUid();
+
+    /**
+     * Returns the unique identifier of the tenant that owns the rule
+     *
+     * @return unique identifier of the tenant that owns the rule.
+     *
+     */
+    TenantId getTenantId();
+
+    /**
+     * Returns the unique identifier of the rule
+     * 
+     * @return unique identifier of the rule.
+     */
+    RuleId getRuleId();
+
+    /**
+     * Returns the serializable message payload.
+     * 
+     * @return the serializable message payload.
+     */
+    V getPayload();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java
new file mode 100644
index 0000000..6768042
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ResponsePluginToRuleMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+
+import java.util.UUID;
+
+public class ResponsePluginToRuleMsg extends AbstractPluginToRuleMsg<ToDeviceMsg>{
+
+    private static final long serialVersionUID = 1L;
+
+    public ResponsePluginToRuleMsg(UUID uid, TenantId tenantId, RuleId ruleId, ToDeviceMsg payload) {
+        super(uid, tenantId, ruleId, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java
new file mode 100644
index 0000000..1f3768e
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcError.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum RpcError {
+    NOT_FOUND, FORBIDDEN, NO_ACTIVE_CONNECTION, TIMEOUT, INTERNAL;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..8826979
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+
+public class RpcRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<ToServerRpcRequestMsg> {
+
+    private static final long serialVersionUID = 1L;
+
+    public RpcRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, ToServerRpcRequestMsg payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java
new file mode 100644
index 0000000..6ddd56a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RpcResponsePluginToRuleMsg.java
@@ -0,0 +1,32 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+
+import java.util.UUID;
+
+public class RpcResponsePluginToRuleMsg extends AbstractPluginToRuleMsg<ToServerRpcResponseMsg> {
+
+    private static final long serialVersionUID = 1L;
+
+    public RpcResponsePluginToRuleMsg(UUID uid, TenantId tenantId, RuleId ruleId, ToServerRpcResponseMsg payload) {
+        super(uid, tenantId, ruleId, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java
new file mode 100644
index 0000000..548bf8e
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/RuleToPluginMsg.java
@@ -0,0 +1,61 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * The basic interface for messages that are sent from particular rule to plugin
+ * instance
+ * 
+ * @author ashvayka
+ *
+ */
+public interface RuleToPluginMsg<V extends Serializable> extends Serializable {
+    
+    /**
+     * Returns the unique identifier of the message
+     * 
+     * @return unique identifier of the message.
+     */
+    UUID getUid();
+
+    
+    /**
+     * Returns the unique identifier of the device that send the message
+     * 
+     * @return unique identifier of the device.
+     */
+    DeviceId getDeviceId();
+
+    /**
+     * Returns the unique identifier of the customer that owns the device
+     *
+     * @return unique identifier of the device.
+     */
+    CustomerId getCustomerId();
+
+    /**
+     * Returns the serializable message payload.
+     * 
+     * @return the serializable message payload.
+     */
+    V getPayload();
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..c72cdb3
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TelemetryUploadRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+
+public class TelemetryUploadRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<TelemetryUploadRequest> {
+
+    private static final long serialVersionUID = 1L;
+
+    public TelemetryUploadRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, TelemetryUploadRequest payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java
new file mode 100644
index 0000000..6d96ae7
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutIntMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+/**
+ * @author Andrew Shvayka
+ */
+public final class TimeoutIntMsg extends TimeoutMsg<Integer> {
+
+    public TimeoutIntMsg(Integer id, long timeout) {
+        super(id, timeout);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java
new file mode 100644
index 0000000..8932c33
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class TimeoutMsg<T> {
+    private final T id;
+    private final long timeout;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java
new file mode 100644
index 0000000..47b00a2
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/TimeoutUUIDMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public final class TimeoutUUIDMsg extends TimeoutMsg<UUID> {
+
+    public TimeoutUUIDMsg(UUID id, long timeout) {
+        super(id, timeout);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
new file mode 100644
index 0000000..3fdc076
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequest.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequest implements Serializable {
+    private final UUID id;
+    private final TenantId tenantId;
+    private final DeviceId deviceId;
+    private final boolean oneway;
+    private final long expirationTime;
+    private final ToDeviceRpcRequestBody body;
+}
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java
new file mode 100644
index 0000000..c37e576
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestBody.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToDeviceRpcRequestBody implements Serializable {
+    private final String method;
+    private final String params;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java
new file mode 100644
index 0000000..c19c9dc
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToDeviceRpcRequestPluginMsg.java
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.device.ToDeviceActorNotificationMsg;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+@RequiredArgsConstructor
+public class ToDeviceRpcRequestPluginMsg implements ToDeviceActorNotificationMsg {
+
+    private final ServerAddress serverAddress;
+    @Getter
+    private final PluginId pluginId;
+    @Getter
+    private final TenantId pluginTenantId;
+    @Getter
+    private final ToDeviceRpcRequest msg;
+
+    public ToDeviceRpcRequestPluginMsg(PluginId pluginId, TenantId pluginTenantId, ToDeviceRpcRequest msg) {
+        this(null, pluginId, pluginTenantId, msg);
+    }
+
+    public Optional<ServerAddress> getServerAddress() {
+        return Optional.ofNullable(serverAddress);
+    }
+
+    @Override
+    public DeviceId getDeviceId() {
+        return msg.getDeviceId();
+    }
+
+    @Override
+    public TenantId getTenantId() {
+        return msg.getTenantId();
+    }
+}
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java
new file mode 100644
index 0000000..22a4faf
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginActorMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.aware.PluginAwareMsg;
+
+public interface ToPluginActorMsg extends PluginAwareMsg {
+
+    TenantId getPluginTenantId();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java
new file mode 100644
index 0000000..f761850
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/ToPluginRpcResponseDeviceMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class ToPluginRpcResponseDeviceMsg implements ToPluginActorMsg {
+    private final PluginId pluginId;
+    private final TenantId pluginTenantId;
+    private final FromDeviceRpcResponse response;
+}
+
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java
new file mode 100644
index 0000000..a427d65
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/msg/UpdateAttributesRequestRuleToPluginMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.msg;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
+
+public class UpdateAttributesRequestRuleToPluginMsg extends AbstractRuleToPluginMsg<UpdateAttributesRequest> {
+
+    private static final long serialVersionUID = 1L;
+
+    public UpdateAttributesRequestRuleToPluginMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId, UpdateAttributesRequest payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java
new file mode 100644
index 0000000..15c12d4
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/Plugin.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+public interface Plugin<T> extends ConfigurableComponent<T> {
+
+    void process(PluginContext ctx, PluginWebsocketMsg<?> wsMsg);
+
+    void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException;
+
+    void process(PluginContext ctx, PluginRestMsg msg);
+
+    void process(PluginContext ctx, RpcMsg msg);
+
+    void process(PluginContext ctx, FromDeviceRpcResponse msg);
+
+    void process(PluginContext ctx, TimeoutMsg<?> msg);
+
+    void onServerAdded(PluginContext ctx, ServerAddress server);
+
+    void onServerRemoved(PluginContext ctx, ServerAddress server);
+
+    void resume(PluginContext ctx);
+
+    void suspend(PluginContext ctx);
+
+    void stop(PluginContext ctx);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java
new file mode 100644
index 0000000..2d71f1a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginAction.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleLifecycleComponent;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+
+import java.util.Optional;
+
+public interface PluginAction<T> extends ConfigurableComponent<T>, RuleLifecycleComponent {
+
+    Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData deviceMsgMd);
+
+    Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response);
+
+    boolean isOneWayAction();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
new file mode 100644
index 0000000..ff02567
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginApiCallSecurityContext.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.io.Serializable;
+
+public final class PluginApiCallSecurityContext implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private final TenantId pluginTenantId;
+    private final PluginId pluginId;
+    private final TenantId tenantId;
+    private final CustomerId customerId;
+
+    public PluginApiCallSecurityContext(TenantId pluginTenantId, PluginId pluginId, TenantId tenantId, CustomerId customerId) {
+        super();
+        this.pluginTenantId = pluginTenantId;
+        this.pluginId = pluginId;
+        this.tenantId = tenantId;
+        this.customerId = customerId;
+    }
+
+    public TenantId getPluginTenantId(){
+        return pluginTenantId;
+    }
+
+    public PluginId getPluginId() {
+        return pluginId;
+    }
+
+    public boolean isSystemAdmin() {
+        return tenantId == null || EntityId.NULL_UUID.equals(tenantId.getId());
+    }
+
+    public boolean isTenantAdmin() {
+        return !isSystemAdmin() && (customerId == null || EntityId.NULL_UUID.equals(customerId.getId()));
+    }
+
+    public boolean isCustomerUser() {
+        return !isSystemAdmin() && !isTenantAdmin();
+    }
+
+    public TenantId getTenantId() {
+        return tenantId;
+    }
+
+    public CustomerId getCustomerId() {
+        return customerId;
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java
new file mode 100644
index 0000000..281a3af
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginCallback.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface PluginCallback<T> {
+
+    void onSuccess(PluginContext ctx, T value);
+
+    void onFailure(PluginContext ctx, Exception e);
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
new file mode 100644
index 0000000..13a9561
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginConstants.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class PluginConstants {
+    public static final String PLUGIN_URL_PREFIX = "/api/plugins";
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
new file mode 100644
index 0000000..834ec3a
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginContext.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.data.kv.TsKvQuery;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+
+public interface PluginContext {
+
+    PluginId getPluginId();
+
+    void reply(PluginToRuleMsg<?> msg);
+
+    boolean checkAccess(DeviceId deviceId);
+
+    Optional<PluginApiCallSecurityContext> getSecurityCtx();
+
+    /*
+        Device RPC API
+     */
+
+    Optional<ServerAddress> resolve(DeviceId deviceId);
+
+    void getDevice(DeviceId deviceId, PluginCallback<Device> pluginCallback);
+
+    void sendRpcRequest(ToDeviceRpcRequest msg);
+
+    void scheduleTimeoutMsg(TimeoutMsg<?> timeoutMsg);
+
+
+    /*
+        Websocket API
+     */
+
+    void send(PluginWebsocketMsg<?> wsMsg) throws IOException;
+
+    void close(PluginWebsocketSessionRef sessionRef) throws IOException;
+
+    /*
+        Plugin RPC API
+     */
+
+    void sendPluginRpcMsg(RpcMsg msg);
+
+    /*
+        Timeseries API
+     */
+
+
+    void saveTsData(DeviceId deviceId, TsKvEntry entry, PluginCallback<Void> callback);
+
+    void saveTsData(DeviceId deviceId, List<TsKvEntry> entry, PluginCallback<Void> callback);
+
+    List<TsKvEntry> loadTimeseries(DeviceId deviceId, TsKvQuery query);
+
+    void loadLatestTimeseries(DeviceId deviceId, Collection<String> keys, PluginCallback<List<TsKvEntry>> callback);
+
+    void loadLatestTimeseries(DeviceId deviceId, PluginCallback<List<TsKvEntry>> callback);
+
+    /*
+        Attributes API
+     */
+
+    void saveAttributes(DeviceId deviceId, String attributeType, List<AttributeKvEntry> attributes, PluginCallback<Void> callback);
+
+    Optional<AttributeKvEntry> loadAttribute(DeviceId deviceId, String attributeType, String attributeKey);
+
+    List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType, List<String> attributeKeys);
+
+    List<AttributeKvEntry> loadAttributes(DeviceId deviceId, String attributeType);
+
+    void removeAttributes(DeviceId deviceId, String scope, List<String> attributeKeys);
+
+    void getCustomerDevices(TenantId tenantId, CustomerId customerId, int limit, PluginCallback<List<Device>> callback);
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java
new file mode 100644
index 0000000..5f39ef9
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginException.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+public class PluginException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PluginException(String msg, Exception e) {
+        super(msg, e);
+    }
+    
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java
new file mode 100644
index 0000000..e4eb432
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/PluginInitializationException.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins;
+
+public class PluginInitializationException extends PluginException {
+
+    private static final long serialVersionUID = 1L;
+
+    public PluginInitializationException(String msg, Exception e) {
+        super(msg, e);
+    }
+    
+    public PluginInitializationException(String msg) {
+        super(msg, null);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java
new file mode 100644
index 0000000..3a17638
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/BasicPluginRestMsg.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+
+@SuppressWarnings("rawtypes")
+public class BasicPluginRestMsg implements PluginRestMsg {
+
+    private final PluginApiCallSecurityContext securityCtx;
+    private final RestRequest request;
+    private final DeferredResult<ResponseEntity> responseHolder;
+
+    public BasicPluginRestMsg(PluginApiCallSecurityContext securityCtx, RestRequest request,
+                              DeferredResult<ResponseEntity> responseHolder) {
+        super();
+        this.securityCtx = securityCtx;
+        this.request = request;
+        this.responseHolder = responseHolder;
+    }
+
+    @Override
+    public PluginApiCallSecurityContext getSecurityCtx() {
+        return securityCtx;
+    }
+
+    @Override
+    public RestRequest getRequest() {
+        return request;
+    }
+
+    @Override
+    public DeferredResult<ResponseEntity> getResponseHolder() {
+        return responseHolder;
+    }
+
+    @Override
+    public PluginId getPluginId() {
+        return securityCtx.getPluginId();
+    }
+
+    @Override
+    public TenantId getPluginTenantId() {
+        return securityCtx.getPluginTenantId();
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java
new file mode 100644
index 0000000..b2ba689
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/PluginRestMsg.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.rest;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+
+@SuppressWarnings("rawtypes")
+public interface PluginRestMsg extends ToPluginActorMsg {
+
+    RestRequest getRequest();
+
+    DeferredResult<ResponseEntity> getResponseHolder();
+    
+    PluginApiCallSecurityContext getSecurityCtx();
+    
+    @Override
+    default PluginId getPluginId() {
+        return getSecurityCtx().getPluginId();
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java
new file mode 100644
index 0000000..c9aaefd
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rest/RestRequest.java
@@ -0,0 +1,87 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.rest;
+
+import lombok.Data;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.RequestEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.MissingServletRequestParameterException;
+import org.thingsboard.server.extensions.api.plugins.PluginConstants;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import java.nio.charset.Charset;
+import java.util.Optional;
+import java.util.function.Function;
+
+@Data
+public class RestRequest {
+    private static final Charset UTF8 = Charset.forName("UTF-8");
+    private final RequestEntity<byte[]> requestEntity;
+    private final HttpServletRequest request;
+
+    public HttpMethod getMethod() {
+        return requestEntity.getMethod();
+    }
+
+    public String getRequestBody() {
+        return new String(requestEntity.getBody(), UTF8);
+    }
+
+    public String[] getPathParams() {
+        String requestUrl = request.getRequestURL().toString();
+        int index = requestUrl.indexOf(PluginConstants.PLUGIN_URL_PREFIX);
+        String[] pathParams = requestUrl.substring(index + PluginConstants.PLUGIN_URL_PREFIX.length()).split("/");
+        String[] result = new String[pathParams.length - 2];
+        System.arraycopy(pathParams, 2, result, 0, result.length);
+        return result;
+    }
+
+    public String getParameter(String paramName) throws ServletException {
+        return getParameter(paramName, null);
+    }
+
+    public String getParameter(String paramName, String defaultValue) throws ServletException {
+        String paramValue = request.getParameter(paramName);
+        if (StringUtils.isEmpty(paramValue)) {
+            if (defaultValue == null) {
+                throw new MissingServletRequestParameterException(paramName, "String");
+            } else {
+                return defaultValue;
+            }
+        } else {
+            return paramValue;
+        }
+    }
+
+    public Optional<Long> getLongParamValue(String paramName) {
+        return getParamValue(paramName, s -> Long.valueOf(s));
+    }
+
+    public Optional<Integer> getIntParamValue(String paramName) {
+        return getParamValue(paramName, s -> Integer.valueOf(s));
+    }
+
+    public <T> Optional<T> getParamValue(String paramName, Function<String, T> function) {
+        String paramValue = request.getParameter(paramName);
+        if (paramValue != null) {
+            return Optional.of(function.apply(paramValue));
+        } else {
+            return Optional.empty();
+        }
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java
new file mode 100644
index 0000000..7aaaf01
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/PluginRpcMsg.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.rpc;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+
+@ToString
+@RequiredArgsConstructor
+public class PluginRpcMsg implements ToPluginActorMsg {
+
+    private final TenantId tenantId;
+    private final PluginId pluginId;
+    @Getter
+    private final RpcMsg rpcMsg;
+
+    @Override
+    public TenantId getPluginTenantId() {
+        return tenantId;
+    }
+
+    @Override
+    public PluginId getPluginId() {
+        return pluginId;
+    }
+
+
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java
new file mode 100644
index 0000000..71a8534
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/rpc/RpcMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.rpc;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+/**
+ * @author Andrew Shvayka
+ */
+@ToString
+@RequiredArgsConstructor
+public class RpcMsg {
+    @Getter
+    private final ServerAddress serverAddress;
+    @Getter
+    private final int msgClazz;
+    @Getter
+    private final byte[] msgData;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java
new file mode 100644
index 0000000..2f4c582
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/BasicPluginWebsocketSessionRef.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Map;
+
+public class BasicPluginWebsocketSessionRef implements PluginWebsocketSessionRef {
+
+    private static final long serialVersionUID = 1L;
+
+    private final String sessionId;
+    private final PluginApiCallSecurityContext securityCtx;
+    private final URI uri;
+    private final Map<String, Object> attributes;
+    private final InetSocketAddress localAddress;
+    private final InetSocketAddress remoteAddress;
+
+    public BasicPluginWebsocketSessionRef(String sessionId, PluginApiCallSecurityContext securityCtx, URI uri, Map<String, Object> attributes,
+            InetSocketAddress localAddress, InetSocketAddress remoteAddress) {
+        super();
+        this.sessionId = sessionId;
+        this.securityCtx = securityCtx;
+        this.uri = uri;
+        this.attributes = attributes;
+        this.localAddress = localAddress;
+        this.remoteAddress = remoteAddress;
+    }
+
+    public String getSessionId() {
+        return sessionId;
+    }
+
+    public TenantId getPluginTenantId() {
+        return securityCtx.getPluginTenantId();
+    }
+
+    public PluginId getPluginId() {
+        return securityCtx.getPluginId();
+    }
+
+    public URI getUri() {
+        return uri;
+    }
+
+    public Map<String, Object> getAttributes() {
+        return attributes;
+    }
+
+    public InetSocketAddress getLocalAddress() {
+        return localAddress;
+    }
+
+    public InetSocketAddress getRemoteAddress() {
+        return remoteAddress;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((sessionId == null) ? 0 : sessionId.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        BasicPluginWebsocketSessionRef other = (BasicPluginWebsocketSessionRef) obj;
+        if (sessionId == null) {
+            if (other.sessionId != null)
+                return false;
+        } else if (!sessionId.equals(other.sessionId))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "BasicPluginWebsocketSessionRef [sessionId=" + sessionId + ", pluginId=" + getPluginId() + "]";
+    }
+
+    @Override
+    public PluginApiCallSecurityContext getSecurityCtx() {
+        return securityCtx;
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java
new file mode 100644
index 0000000..ef70edc
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/AbstractPluginWebSocketMsg.java
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public abstract class AbstractPluginWebSocketMsg<T> implements PluginWebsocketMsg<T> {
+
+    private static final long serialVersionUID = 1L;
+
+    private final PluginWebsocketSessionRef sessionRef;
+    private final T payload;
+
+    AbstractPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, T payload) {
+        this.sessionRef = sessionRef;
+        this.payload = payload;
+    }
+
+    public PluginWebsocketSessionRef getSessionRef() {
+        return sessionRef;
+    }
+
+
+    @Override
+    public TenantId getPluginTenantId(){
+        return sessionRef.getPluginTenantId();
+    }
+
+    @Override
+    public PluginId getPluginId() {
+        return sessionRef.getPluginId();
+    }
+
+    @Override
+    public PluginApiCallSecurityContext getSecurityCtx() {
+        return sessionRef.getSecurityCtx();
+    }
+
+    public T getPayload() {
+        return payload;
+    }
+
+    @Override
+    public String toString() {
+        return "AbstractPluginWebSocketMsg [sessionRef=" + sessionRef + ", payload=" + payload + "]";
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java
new file mode 100644
index 0000000..8afe0ef
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/BinaryPluginWebSocketMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+import java.nio.ByteBuffer;
+
+public class BinaryPluginWebSocketMsg extends AbstractPluginWebSocketMsg<ByteBuffer> {
+
+    private static final long serialVersionUID = 1L;
+
+    public BinaryPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, ByteBuffer payload) {
+        super(sessionRef, payload);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java
new file mode 100644
index 0000000..a02c997
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/EmptyPluginWebsocketMsg.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+import java.nio.ByteBuffer;
+
+public class EmptyPluginWebsocketMsg extends AbstractPluginWebSocketMsg<ByteBuffer> {
+
+    private static final long serialVersionUID = 1L;
+    private static ByteBuffer EMPTY = ByteBuffer.wrap(new byte[0]);
+
+    protected EmptyPluginWebsocketMsg(PluginWebsocketSessionRef sessionRef) {
+        super(sessionRef, EMPTY);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java
new file mode 100644
index 0000000..7d6d682
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PingPluginWebsocketMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public class PingPluginWebsocketMsg extends EmptyPluginWebsocketMsg {
+
+    private static final long serialVersionUID = 1L;
+
+    public PingPluginWebsocketMsg(PluginWebsocketSessionRef sessionRef) {
+        super(sessionRef);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java
new file mode 100644
index 0000000..8cc4232
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PluginWebsocketMsg.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+import org.thingsboard.server.extensions.api.plugins.msg.ToPluginActorMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+import java.io.Serializable;
+
+public interface PluginWebsocketMsg<T> extends ToPluginActorMsg, Serializable {
+
+    PluginWebsocketSessionRef getSessionRef();
+
+    T getPayload();
+
+    PluginApiCallSecurityContext getSecurityCtx();
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java
new file mode 100644
index 0000000..ab5a238
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/PongPluginWebsocketMsg.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public class PongPluginWebsocketMsg extends EmptyPluginWebsocketMsg {
+
+    private static final long serialVersionUID = 1L;
+
+    public PongPluginWebsocketMsg(PluginWebsocketSessionRef sessionRef) {
+        super(sessionRef);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java
new file mode 100644
index 0000000..a365498
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/SessionEventPluginWebSocketMsg.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.SessionEvent;
+
+public class SessionEventPluginWebSocketMsg extends AbstractPluginWebSocketMsg<SessionEvent> {
+
+    private static final long serialVersionUID = 1L;
+
+    public SessionEventPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, SessionEvent payload) {
+        super(sessionRef, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java
new file mode 100644
index 0000000..d144b6c
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/msg/TextPluginWebSocketMsg.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws.msg;
+
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+
+public class TextPluginWebSocketMsg extends AbstractPluginWebSocketMsg<String> {
+
+    private static final long serialVersionUID = 1L;
+    
+    public TextPluginWebSocketMsg(PluginWebsocketSessionRef sessionRef, String payload) {
+        super(sessionRef, payload);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java
new file mode 100644
index 0000000..7079c82
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/PluginWebsocketSessionRef.java
@@ -0,0 +1,45 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws;
+
+import org.thingsboard.server.common.data.id.PluginId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.PluginApiCallSecurityContext;
+
+import java.io.Serializable;
+import java.net.InetSocketAddress;
+import java.net.URI;
+import java.util.Map;
+
+public interface PluginWebsocketSessionRef extends Serializable {
+
+    String getSessionId();
+
+    TenantId getPluginTenantId();
+
+    PluginId getPluginId();
+
+    URI getUri();
+
+    Map<String, Object> getAttributes();
+
+    InetSocketAddress getLocalAddress();
+
+    InetSocketAddress getRemoteAddress();
+
+    PluginApiCallSecurityContext getSecurityCtx();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java
new file mode 100644
index 0000000..66d3f27
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/SessionEvent.java
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws;
+
+import lombok.Getter;
+import lombok.ToString;
+
+import java.util.Optional;
+
+@ToString
+public class SessionEvent {
+
+    public enum SessionEventType {
+        ESTABLISHED, CLOSED, ERROR
+    };
+
+    @Getter
+    private final SessionEventType eventType;
+    @Getter
+    private final Optional<Throwable> error;
+
+    private SessionEvent(SessionEventType eventType, Throwable error) {
+        super();
+        this.eventType = eventType;
+        this.error = Optional.ofNullable(error);
+    }
+
+    public static SessionEvent onEstablished() {
+        return new SessionEvent(SessionEventType.ESTABLISHED, null);
+    }
+
+    public static SessionEvent onClosed() {
+        return new SessionEvent(SessionEventType.CLOSED, null);
+    }
+
+    public static SessionEvent onError(Throwable t) {
+        return new SessionEvent(SessionEventType.ERROR, t);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java
new file mode 100644
index 0000000..0ee8830
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/plugins/ws/WsSessionMetaData.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.plugins.ws;
+
+public class WsSessionMetaData {
+
+    private PluginWebsocketSessionRef sessionRef;
+    private long lastActivityTime;
+
+    public WsSessionMetaData(PluginWebsocketSessionRef sessionRef) {
+        super();
+        this.sessionRef = sessionRef;
+        this.lastActivityTime = System.currentTimeMillis();
+    }
+
+    public PluginWebsocketSessionRef getSessionRef() {
+        return sessionRef;
+    }
+
+    public void setSessionRef(PluginWebsocketSessionRef sessionRef) {
+        this.sessionRef = sessionRef;
+    }
+
+    public long getLastActivityTime() {
+        return lastActivityTime;
+    }
+
+    public void setLastActivityTime(long lastActivityTime) {
+        this.lastActivityTime = lastActivityTime;
+    }
+
+    @Override
+    public String toString() {
+        return "WsSessionMetaData [sessionRef=" + sessionRef + ", lastActivityTime=" + lastActivityTime + "]";
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java
new file mode 100644
index 0000000..337028f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleContext.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+
+import java.util.Optional;
+
+public interface RuleContext {
+
+    RuleId getRuleId();
+
+    DeviceAttributes getDeviceAttributes();
+
+    Event save(Event event);
+
+    Optional<Event> saveIfNotExists(Event event);
+
+    Optional<Event> findEvent(String eventType, String eventUid);
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java
new file mode 100644
index 0000000..7cc3bd4
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleException.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+public class RuleException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public RuleException(String msg) {
+        super(msg);
+    }
+
+    public RuleException(String msg, Exception e) {
+        super(msg, e);
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleFilter.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleFilter.java
new file mode 100644
index 0000000..0f39f92
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleFilter.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleFilter<T> extends ConfigurableComponent<T>, RuleLifecycleComponent {
+
+    boolean filter(RuleContext ctx, ToDeviceActorMsg msg);
+    
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java
new file mode 100644
index 0000000..8cf19e5
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleInitializationException.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+public class RuleInitializationException extends RuleException {
+
+    private static final long serialVersionUID = 1L;
+
+    public RuleInitializationException(String msg, Exception e) {
+        super(msg, e);
+    }
+    
+    public RuleInitializationException(String msg) {
+        super(msg, null);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java
new file mode 100644
index 0000000..4eb38d2
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleLifecycleComponent.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleLifecycleComponent {
+
+    void resume();
+
+    void suspend();
+
+    void stop();
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java
new file mode 100644
index 0000000..8f528a9
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessingMetaData.java
@@ -0,0 +1,43 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+public class RuleProcessingMetaData {
+
+    private final Map<String, Object> md;
+
+    public RuleProcessingMetaData() {
+        super();
+        this.md = new HashMap<>();
+    }
+
+    public <T> void put(String key, T value) {
+        md.put(key, value);
+    }
+
+    public <T> Optional<T> get(String key) {
+        return Optional.ofNullable((T) md.get(key));
+    }
+
+    public Map<String, Object> getValues() {
+        return Collections.unmodifiableMap(md);
+    }
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java
new file mode 100644
index 0000000..179204f
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/RuleProcessor.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.ConfigurableComponent;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface RuleProcessor<T> extends ConfigurableComponent<T>, RuleLifecycleComponent {
+
+    RuleProcessingMetaData process(RuleContext ctx, ToDeviceActorMsg msg) throws RuleException;
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java
new file mode 100644
index 0000000..7da902d
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/SimpleRuleLifecycleComponent.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public abstract class SimpleRuleLifecycleComponent implements RuleLifecycleComponent {
+
+    @Override
+    public void resume() {
+        log.debug("Resume method was called, but no impl provided!");
+    }
+
+    @Override
+    public void suspend() {
+        log.debug("Suspend method was called, but no impl provided!");
+    }
+
+    @Override
+    public void stop() {
+        log.debug("Stop method was called, but no impl provided!");
+    }
+
+}
diff --git a/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java
new file mode 100644
index 0000000..57ac998
--- /dev/null
+++ b/extensions-api/src/main/java/org/thingsboard/server/extensions/api/rules/ToRuleActorMsg.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.api.rules;
+
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
+
+public interface ToRuleActorMsg extends TenantAwareMsg {
+
+    RuleId getRuleId();
+    
+}
diff --git a/extensions-api/src/main/resources/EmptyJsonDescriptor.json b/extensions-api/src/main/resources/EmptyJsonDescriptor.json
new file mode 100644
index 0000000..03bf145
--- /dev/null
+++ b/extensions-api/src/main/resources/EmptyJsonDescriptor.json
@@ -0,0 +1,12 @@
+{
+  "schema": {
+    "description": "Empty Schema",
+    "type": "object",
+    "additionalProperties": false,
+    "properties": {
+    }
+  },
+  "form": [
+    "*"
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/pom.xml b/extensions-core/pom.xml
new file mode 100644
index 0000000..b2e2d18
--- /dev/null
+++ b/extensions-core/pom.xml
@@ -0,0 +1,131 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>server</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server</groupId>
+    <artifactId>extensions-core</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Core Extensions</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server</groupId>
+            <artifactId>extensions-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.code.gson</groupId>
+            <artifactId>gson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.velocity</groupId>
+            <artifactId>velocity-tools</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.protobuf</groupId>
+            <artifactId>protobuf-java</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-dependency-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.xolstice.maven.plugins</groupId>
+                <artifactId>protobuf-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>build-helper-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java
new file mode 100644
index 0000000..33b1333
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailAction.java
@@ -0,0 +1,109 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.mail;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+import org.thingsboard.server.extensions.core.utils.VelocityUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Action(name = "Send Mail Action", descriptor = "SendMailActionDescriptor.json", configuration = SendMailActionConfiguration.class)
+@Slf4j
+public class SendMailAction extends SimpleRuleLifecycleComponent implements PluginAction<SendMailActionConfiguration> {
+
+    private SendMailActionConfiguration configuration;
+    private Optional<Template> fromTemplate;
+    private Optional<Template> toTemplate;
+    private Optional<Template> ccTemplate;
+    private Optional<Template> bccTemplate;
+    private Optional<Template> subjectTemplate;
+    private Optional<Template> bodyTemplate;
+
+    @Override
+    public void init(SendMailActionConfiguration configuration) {
+        this.configuration = configuration;
+        try {
+            fromTemplate = toTemplate(configuration.getFromTemplate(), "From Template");
+            toTemplate = toTemplate(configuration.getToTemplate(), "To Template");
+            ccTemplate = toTemplate(configuration.getCcTemplate(), "Cc Template");
+            bccTemplate = toTemplate(configuration.getBccTemplate(), "Bcc Template");
+            subjectTemplate = toTemplate(configuration.getSubjectTemplate(), "Subject Template");
+            bodyTemplate = toTemplate(configuration.getBodyTemplate(), "Body Template");
+        } catch (ParseException e) {
+            log.error("Failed to create templates based on provided configuration!", e);
+            throw new RuntimeException("Failed to create templates based on provided configuration!", e);
+        }
+    }
+
+    private Optional<Template> toTemplate(String source, String name) throws ParseException {
+        if (!StringUtils.isEmpty(source)) {
+            return Optional.of(VelocityUtils.create(source, name));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData metadata) {
+        String sendFlag = configuration.getSendFlag();
+        if (StringUtils.isEmpty(sendFlag) || (Boolean) metadata.get(sendFlag).orElse(Boolean.FALSE)) {
+            VelocityContext context = VelocityUtils.createContext(metadata);
+
+            SendMailActionMsg.SendMailActionMsgBuilder builder = SendMailActionMsg.builder();
+            fromTemplate.ifPresent(t -> builder.from(VelocityUtils.merge(t, context)));
+            toTemplate.ifPresent(t -> builder.to(VelocityUtils.merge(t, context)));
+            ccTemplate.ifPresent(t -> builder.cc(VelocityUtils.merge(t, context)));
+            bccTemplate.ifPresent(t -> builder.bcc(VelocityUtils.merge(t, context)));
+            subjectTemplate.ifPresent(t -> builder.subject(VelocityUtils.merge(t, context)));
+            bodyTemplate.ifPresent(t -> builder.body(VelocityUtils.merge(t, context)));
+            return Optional.of(new SendMailRuleToPluginActionMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(), toDeviceActorMsg.getDeviceId(),
+                    builder.build()));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+        if (response instanceof ResponsePluginToRuleMsg) {
+            return Optional.of(((ResponsePluginToRuleMsg) response).getPayload());
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public boolean isOneWayAction() {
+        return true;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java
new file mode 100644
index 0000000..bec33b9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionConfiguration.java
@@ -0,0 +1,34 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.mail;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class SendMailActionConfiguration {
+
+    private String sendFlag;
+
+    private String fromTemplate;
+    private String toTemplate;
+    private String ccTemplate;
+    private String bccTemplate;
+    private String subjectTemplate;
+    private String bodyTemplate;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java
new file mode 100644
index 0000000..979dfe0
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailActionMsg.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.mail;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@Builder
+public class SendMailActionMsg implements Serializable {
+
+    private final String from;
+    private final String to;
+    private final String cc;
+    private final String bcc;
+    private final String subject;
+    private final String body;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java
new file mode 100644
index 0000000..ab120ad
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/mail/SendMailRuleToPluginActionMsg.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.mail;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.plugins.msg.AbstractRuleToPluginMsg;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class SendMailRuleToPluginActionMsg extends AbstractRuleToPluginMsg<SendMailActionMsg> {
+
+    public SendMailRuleToPluginActionMsg(TenantId tenantId, CustomerId customerId, DeviceId deviceId,
+                                     SendMailActionMsg payload) {
+        super(tenantId, customerId, deviceId, payload);
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java
new file mode 100644
index 0000000..e23e61e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/rpc/RpcPluginAction.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.rpc;
+
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.util.Optional;
+
+@Action(name = "RPC Plugin Action")
+public class RpcPluginAction extends SimpleRuleLifecycleComponent implements PluginAction<EmptyComponentConfiguration> {
+
+    public void init(EmptyComponentConfiguration configuration) {
+    }
+
+    @Override
+    public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData deviceMsgMd) {
+        FromDeviceMsg msg = toDeviceActorMsg.getPayload();
+        if (msg.getMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+            ToServerRpcRequestMsg payload = (ToServerRpcRequestMsg) msg;
+            return Optional.of(new RpcRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+                    toDeviceActorMsg.getDeviceId(), payload));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+        if (response instanceof RpcResponsePluginToRuleMsg) {
+            return Optional.of(((RpcResponsePluginToRuleMsg) response).getPayload());
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public boolean isOneWayAction() {
+        return false;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java
new file mode 100644
index 0000000..cfbccd9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/telemetry/TelemetryPluginAction.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.telemetry;
+
+import org.thingsboard.server.common.msg.core.GetAttributesRequest;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Action;
+import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.util.Optional;
+
+@Action(name = "Telemetry Plugin Action")
+public class TelemetryPluginAction extends SimpleRuleLifecycleComponent implements PluginAction<EmptyComponentConfiguration> {
+
+    public void init(EmptyComponentConfiguration configuration) {
+    }
+
+    @Override
+    public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg toDeviceActorMsg, RuleProcessingMetaData deviceMsgMd) {
+        FromDeviceMsg msg = toDeviceActorMsg.getPayload();
+        if (msg.getMsgType() == MsgType.POST_TELEMETRY_REQUEST) {
+            TelemetryUploadRequest payload = (TelemetryUploadRequest) msg;
+            return Optional.of(new TelemetryUploadRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+                    toDeviceActorMsg.getDeviceId(), payload));
+        } else if (msg.getMsgType() == MsgType.POST_ATTRIBUTES_REQUEST) {
+            UpdateAttributesRequest payload = (UpdateAttributesRequest) msg;
+            return Optional.of(new UpdateAttributesRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+                    toDeviceActorMsg.getDeviceId(), payload));
+        } else if (msg.getMsgType() == MsgType.GET_ATTRIBUTES_REQUEST) {
+            GetAttributesRequest payload = (GetAttributesRequest) msg;
+            return Optional.of(new GetAttributesRequestRuleToPluginMsg(toDeviceActorMsg.getTenantId(), toDeviceActorMsg.getCustomerId(),
+                    toDeviceActorMsg.getDeviceId(), payload));
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    @Override
+    public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+        if (response instanceof ResponsePluginToRuleMsg) {
+            return Optional.of(((ResponsePluginToRuleMsg) response).getPayload());
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public boolean isOneWayAction() {
+        return false;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java
new file mode 100644
index 0000000..d427170
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/AbstractTemplatePluginAction.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.template;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceRequestMsg;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginAction;
+import org.thingsboard.server.extensions.api.plugins.msg.PluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+import org.thingsboard.server.extensions.core.utils.VelocityUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+public abstract class AbstractTemplatePluginAction<T extends TemplateActionConfiguration> extends SimpleRuleLifecycleComponent implements PluginAction<T> {
+    protected T configuration;
+    protected Template template;
+
+    @Override
+    public void init(T configuration) {
+        this.configuration = configuration;
+        try {
+            this.template = VelocityUtils.create(configuration.getTemplate(), "Template");
+        } catch (ParseException e) {
+            throw new RuntimeException(e.getMessage(), e);
+        }
+    }
+
+    @Override
+    public Optional<RuleToPluginMsg<?>> convert(RuleContext ctx, ToDeviceActorMsg msg, RuleProcessingMetaData deviceMsgMd) {
+        FromDeviceRequestMsg payload;
+        if (msg.getPayload() instanceof FromDeviceRequestMsg) {
+            payload = (FromDeviceRequestMsg) msg.getPayload();
+        } else {
+            throw new IllegalArgumentException("Action does not support messages of type: " + msg.getPayload().getMsgType());
+        }
+        return buildRuleToPluginMsg(ctx, msg, payload);
+    }
+
+    @Override
+    public Optional<ToDeviceMsg> convert(PluginToRuleMsg<?> response) {
+        if (response instanceof ResponsePluginToRuleMsg) {
+            return Optional.of(((ResponsePluginToRuleMsg) response).getPayload());
+        }
+        return Optional.empty();
+    }
+
+    protected String getMsgBody(RuleContext ctx, ToDeviceActorMsg msg) {
+        VelocityContext context = VelocityUtils.createContext(ctx.getDeviceAttributes(), msg.getPayload());
+        return VelocityUtils.merge(template, context);
+    }
+
+    abstract protected Optional<RuleToPluginMsg<?>> buildRuleToPluginMsg(RuleContext ctx,
+                                                                         ToDeviceActorMsg msg,
+                                                                         FromDeviceRequestMsg payload);
+
+    @Override
+    public boolean isOneWayAction() {
+        return !configuration.isSync();
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java
new file mode 100644
index 0000000..d9caa97
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/action/template/TemplateActionConfiguration.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.action.template;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface TemplateActionConfiguration {
+
+    boolean isSync();
+    String getTemplate();
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java
new file mode 100644
index 0000000..5abfe92
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/BasicJsFilter.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleFilter;
+
+import javax.script.ScriptException;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public abstract class BasicJsFilter implements RuleFilter<JsFilterConfiguration> {
+
+    protected JsFilterConfiguration configuration;
+    protected NashornJsEvaluator evaluator;
+
+    @Override
+    public void init(JsFilterConfiguration configuration) {
+        this.configuration = configuration;
+        initEvaluator(configuration);
+    }
+
+    @Override
+    public boolean filter(RuleContext ctx, ToDeviceActorMsg msg) {
+        try {
+            return doFilter(ctx, msg);
+        } catch (ScriptException e) {
+            log.warn("RuleFilter evaluation exception: {}", e.getMessage(), e);
+            throw new RuntimeException(e);
+        }
+    }
+
+    protected abstract boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException;
+
+    @Override
+    public void resume() {
+        initEvaluator(configuration);
+    }
+
+    @Override
+    public void suspend() {
+        destroyEvaluator();
+    }
+
+    @Override
+    public void stop() {
+        destroyEvaluator();
+    }
+
+    private void initEvaluator(JsFilterConfiguration configuration) {
+        evaluator = new NashornJsEvaluator(configuration.getFilter());
+    }
+
+    private void destroyEvaluator() {
+        if (evaluator != null) {
+            evaluator.destroy();
+        }
+    }
+
+    protected static Object getValue(KvEntry attr) {
+        switch (attr.getDataType()) {
+            case STRING:
+                return attr.getStrValue().get();
+            case LONG:
+                return attr.getLongValue().get();
+            case DOUBLE:
+                return attr.getDoubleValue().get();
+            case BOOLEAN:
+                return attr.getBooleanValue().get();
+        }
+        return null;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java
new file mode 100644
index 0000000..35ba20e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilter.java
@@ -0,0 +1,93 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.msg.core.UpdateAttributesRequest;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+
+import javax.script.Bindings;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Device Attributes Filter", descriptor = "JsFilterDescriptor.json", configuration = JsFilterConfiguration.class)
+@Slf4j
+public class DeviceAttributesFilter extends BasicJsFilter {
+
+    public static final String CLIENT_SIDE = "cs";
+    public static final String SERVER_SIDE = "ss";
+    public static final String SHARED = "shared";
+
+    @Override
+    protected boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException {
+        return evaluator.execute(toBindings(ctx.getDeviceAttributes(), msg != null ? msg.getPayload() : null));
+    }
+
+    private Bindings toBindings(DeviceAttributes attributes, FromDeviceMsg msg) {
+        Bindings bindings = new SimpleBindings();
+        convertListEntries(bindings, CLIENT_SIDE, attributes.getClientSideAttributes());
+        convertListEntries(bindings, SERVER_SIDE, attributes.getServerSideAttributes());
+        convertListEntries(bindings, SHARED, attributes.getServerSidePublicAttributes());
+
+        if (msg != null) {
+            switch (msg.getMsgType()) {
+                case POST_ATTRIBUTES_REQUEST:
+                    updateBindings(bindings, (UpdateAttributesRequest) msg);
+                    break;
+            }
+        }
+
+        return bindings;
+    }
+
+    private void updateBindings(Bindings bindings, UpdateAttributesRequest msg) {
+        Map<String, Object> attrMap = (Map<String, Object>) bindings.get(CLIENT_SIDE);
+        for (AttributeKvEntry attr : msg.getAttributes()) {
+            if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey())
+                    && !SHARED.equalsIgnoreCase(attr.getKey())) {
+                bindings.put(attr.getKey(), getValue(attr));
+            }
+            attrMap.put(attr.getKey(), getValue(attr));
+        }
+        bindings.put(CLIENT_SIDE, attrMap);
+    }
+
+    public static Bindings convertListEntries(Bindings bindings, String attributesVarName, Collection<AttributeKvEntry> attributes) {
+        Map<String, Object> attrMap = new HashMap<>();
+        for (AttributeKvEntry attr : attributes) {
+            if (!CLIENT_SIDE.equalsIgnoreCase(attr.getKey()) && !SERVER_SIDE.equalsIgnoreCase(attr.getKey())
+                    && !SHARED.equalsIgnoreCase(attr.getKey())) {
+                bindings.put(attr.getKey(), getValue(attr));
+            }
+            attrMap.put(attr.getKey(), getValue(attr));
+        }
+        bindings.put(attributesVarName, attrMap);
+        return bindings;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java
new file mode 100644
index 0000000..10c0044
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceAttributesFilterConfiguration {
+
+    private String filterBody;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java
new file mode 100644
index 0000000..ea6df9d
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/DeviceTelemetryFilter.java
@@ -0,0 +1,60 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+
+import javax.script.Bindings;
+import javax.script.ScriptException;
+import javax.script.SimpleBindings;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Device Telemetry Filter", descriptor = "JsFilterDescriptor.json", configuration = JsFilterConfiguration.class)
+@Slf4j
+public class DeviceTelemetryFilter extends BasicJsFilter {
+
+    @Override
+    protected boolean doFilter(RuleContext ctx, ToDeviceActorMsg msg) throws ScriptException {
+        FromDeviceMsg deviceMsg = msg.getPayload();
+        if (deviceMsg instanceof TelemetryUploadRequest) {
+            TelemetryUploadRequest telemetryMsg = (TelemetryUploadRequest) deviceMsg;
+            for (List<KvEntry> entries : telemetryMsg.getData().values()) {
+                if (evaluator.execute(toBindings(entries))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private Bindings toBindings(List<KvEntry> entries) {
+        Bindings bindings = new SimpleBindings();
+        for (KvEntry entry : entries) {
+            bindings.put(entry.getKey(), getValue(entry));
+        }
+        return bindings;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java
new file mode 100644
index 0000000..3b64a80
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/JsFilterConfiguration.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class JsFilterConfiguration {
+
+    private final String filter;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
new file mode 100644
index 0000000..21180d7
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilter.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleFilter;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.thingsboard.server.common.msg.session.MsgType.TO_SERVER_RPC_REQUEST;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Method Name Filter", descriptor = "MethodNameFilterDescriptor.json", configuration = MethodNameFilterConfiguration.class)
+@Slf4j
+public class MethodNameFilter extends SimpleRuleLifecycleComponent implements RuleFilter<MethodNameFilterConfiguration> {
+
+    private Set<String> methods;
+
+    @Override
+    public void init(MethodNameFilterConfiguration configuration) {
+        methods = Arrays.asList(configuration.getMethodNames()).stream().map(m -> m.getName()).collect(Collectors.toSet());
+    }
+
+    @Override
+    public boolean filter(RuleContext ctx, ToDeviceActorMsg msg) {
+        if (msg.getPayload().getMsgType() == TO_SERVER_RPC_REQUEST) {
+            return methods.contains(((ToServerRpcRequestMsg) msg.getPayload()).getMethod());
+        } else {
+            return false;
+        }
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java
new file mode 100644
index 0000000..8352e9e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MethodNameFilterConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class MethodNameFilterConfiguration {
+
+    private MethodName[] methodNames;
+
+    @Data
+    public static class MethodName {
+        private String name;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
new file mode 100644
index 0000000..84deea5
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilter.java
@@ -0,0 +1,67 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.extensions.api.component.Filter;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.thingsboard.server.extensions.api.rules.RuleFilter;
+import org.thingsboard.server.extensions.api.rules.SimpleRuleLifecycleComponent;
+
+import java.security.InvalidParameterException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Filter(name = "Message Type Filter", descriptor = "MsgTypeFilterDescriptor.json", configuration = MsgTypeFilterConfiguration.class)
+@Slf4j
+public class MsgTypeFilter extends SimpleRuleLifecycleComponent implements RuleFilter<MsgTypeFilterConfiguration> {
+
+    private List<MsgType> msgTypes;
+
+    @Override
+    public void init(MsgTypeFilterConfiguration configuration) {
+        msgTypes = Arrays.asList(configuration.getMessageTypes()).stream().map(type -> {
+            switch (type) {
+                case "GET_ATTRIBUTES":
+                    return MsgType.GET_ATTRIBUTES_REQUEST;
+                case "POST_ATTRIBUTES":
+                    return MsgType.POST_ATTRIBUTES_REQUEST;
+                case "POST_TELEMETRY":
+                    return MsgType.POST_TELEMETRY_REQUEST;
+                case "RPC_REQUEST":
+                    return MsgType.TO_SERVER_RPC_REQUEST;
+                default:
+                    throw new InvalidParameterException("Can't map " + type + " to " + MsgType.class.getName() + "!");
+            }
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean filter(RuleContext ctx, ToDeviceActorMsg msg) {
+        for (MsgType msgType : msgTypes) {
+            if (msgType == msg.getPayload().getMsgType()) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java
new file mode 100644
index 0000000..3053188
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/MsgTypeFilterConfiguration.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class MsgTypeFilterConfiguration {
+
+    private String[] messageTypes;
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java
new file mode 100644
index 0000000..365aa4c
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/filter/NashornJsEvaluator.java
@@ -0,0 +1,63 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.script.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class NashornJsEvaluator {
+
+    private static NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
+
+    private CompiledScript engine;
+
+    public NashornJsEvaluator(String script) {
+        engine = compileScript(script);
+    }
+
+    private static CompiledScript compileScript(String script) {
+        ScriptEngine engine = factory.getScriptEngine(new String[]{"--no-java"});
+        Compilable compEngine = (Compilable) engine;
+        try {
+            return compEngine.compile(script);
+        } catch (ScriptException e) {
+            log.warn("Failed to compile filter script: {}", e.getMessage(), e);
+            throw new IllegalArgumentException("Can't compile script: " + e.getMessage());
+        }
+    }
+
+    public Boolean execute(Bindings bindings) throws ScriptException {
+        Object eval = engine.eval(bindings);
+        if (eval instanceof Boolean) {
+            return (Boolean) eval;
+        } else {
+            log.warn("Wrong result type: {}", eval);
+            throw new ScriptException("Wrong result type: " + eval);
+        }
+    }
+
+    public void destroy() {
+        engine = null;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java
new file mode 100644
index 0000000..f0ceefc
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/KeyValuePluginProperties.java
@@ -0,0 +1,27 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class KeyValuePluginProperties {
+    private String key;
+    private String value;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
new file mode 100644
index 0000000..52fd2e9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPlugin.java
@@ -0,0 +1,121 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.mail;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.core.action.mail.SendMailAction;
+import org.thingsboard.server.extensions.core.action.mail.SendMailActionMsg;
+
+import javax.mail.MessagingException;
+import javax.mail.internet.MimeMessage;
+import java.util.Properties;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "Mail Plugin", actions = {SendMailAction.class}, descriptor = "MailPluginDescriptor.json", configuration = MailPluginConfiguration.class)
+@Slf4j
+public class MailPlugin extends AbstractPlugin<MailPluginConfiguration> implements RuleMsgHandler {
+
+    private MailPluginConfiguration configuration;
+    private JavaMailSenderImpl mailSender;
+
+    @Override
+    public void init(MailPluginConfiguration configuration) {
+        log.info("Initializing plugin using configuration {}", configuration);
+        this.configuration = configuration;
+        initMailSender(configuration);
+    }
+
+    @Override
+    public void resume(PluginContext ctx) {
+        initMailSender(configuration);
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+        mailSender = null;
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+        mailSender = null;
+    }
+
+    private void initMailSender(MailPluginConfiguration configuration) {
+        JavaMailSenderImpl mail = new JavaMailSenderImpl();
+        mail.setHost(configuration.getHost());
+        mail.setPort(configuration.getPort());
+        mail.setUsername(configuration.getUsername());
+        mail.setPassword(configuration.getPassword());
+        if (configuration.getOtherProperties() != null) {
+            Properties mailProperties = new Properties();
+            configuration.getOtherProperties()
+                    .stream().forEach(p -> mailProperties.put(p.getKey(), p.getValue()));
+            mail.setJavaMailProperties(mailProperties);
+        }
+        mailSender = mail;
+    }
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        if (msg.getPayload() instanceof SendMailActionMsg) {
+            try {
+                sendMail((SendMailActionMsg) msg.getPayload());
+            } catch (Exception e) {
+                log.warn("Failed to send email", e);
+                throw new RuleException("Failed to send email", e);
+            }
+        } else {
+            throw new RuntimeException("Not supported msg type: " + msg.getPayload().getClass() + "!");
+        }
+    }
+
+    private void sendMail(SendMailActionMsg msg) throws MessagingException {
+        log.debug("Sending mail {}", msg);
+        MimeMessage mailMsg = mailSender.createMimeMessage();
+        MimeMessageHelper helper = new MimeMessageHelper(mailMsg, "UTF-8");
+        helper.setFrom(msg.getFrom());
+        helper.setTo(msg.getTo());
+        if (!StringUtils.isEmpty(msg.getCc())) {
+            helper.setCc(msg.getCc());
+        }
+        if (!StringUtils.isEmpty(msg.getBcc())) {
+            helper.setBcc(msg.getBcc());
+        }
+        helper.setSubject(msg.getSubject());
+        helper.setText(msg.getBody());
+        mailSender.send(helper.getMimeMessage());
+        log.debug("Mail sent {}", msg);
+    }
+
+    @Override
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return this;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java
new file mode 100644
index 0000000..38eb4fc
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/mail/MailPluginConfiguration.java
@@ -0,0 +1,33 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.mail;
+
+import lombok.Data;
+import org.thingsboard.server.extensions.core.plugin.KeyValuePluginProperties;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class MailPluginConfiguration {
+    private String host;
+    private Integer port;
+    private String username;
+    private String password;
+    private List<KeyValuePluginProperties> otherProperties;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java
new file mode 100644
index 0000000..ef7d612
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPlugin.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.messaging;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "Device Messaging Plugin", actions = {RpcPluginAction.class},
+        descriptor = "DeviceMessagingPluginDescriptor.json", configuration = DeviceMessagingPluginConfiguration.class)
+@Slf4j
+public class DeviceMessagingPlugin extends AbstractPlugin<DeviceMessagingPluginConfiguration> {
+
+    private DeviceMessagingRuleMsgHandler ruleHandler;
+
+    public DeviceMessagingPlugin() {
+        ruleHandler = new DeviceMessagingRuleMsgHandler();
+    }
+
+    @Override
+    public void init(DeviceMessagingPluginConfiguration configuration) {
+        ruleHandler.setConfiguration(configuration);
+    }
+
+    @Override
+    public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+        ruleHandler.process(ctx, msg);
+    }
+
+    @Override
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return ruleHandler;
+    }
+
+    @Override
+    public void resume(PluginContext ctx) {
+
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java
new file mode 100644
index 0000000..62580ed
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingPluginConfiguration.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.messaging;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class DeviceMessagingPluginConfiguration {
+
+    private int maxDeviceCountPerCustomer;
+    private long defaultTimeout;
+    private long maxTimeout;
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
new file mode 100644
index 0000000..d029170
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/DeviceMessagingRuleMsgHandler.java
@@ -0,0 +1,228 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.messaging;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class DeviceMessagingRuleMsgHandler implements RuleMsgHandler {
+
+    private static final Gson GSON = new Gson();
+
+    private static final String GET_DEVICE_LIST_METHOD_NAME = "getDevices";
+    private static final String SEND_MSG_METHOD_NAME = "sendMsg";
+    private static final String ON_MSG_METHOD_NAME = "onMsg";
+    private static final String ONEWAY = "oneway";
+    private static final String TIMEOUT = "timeout";
+    private static final String DEVICE_ID = "deviceId";
+
+    private Map<UUID, PendingRpcRequestMetadata> pendingMsgs = new HashMap<>();
+
+    @Setter
+    private DeviceMessagingPluginConfiguration configuration;
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        if (msg.getPayload() instanceof ToServerRpcRequestMsg) {
+            ToServerRpcRequestMsg request = (ToServerRpcRequestMsg) msg.getPayload();
+            try {
+                PendingRpcRequestMetadata md = new PendingRpcRequestMetadata(msg.getUid(),
+                        request.getRequestId(), tenantId, ruleId, msg.getCustomerId(), msg.getDeviceId());
+                switch (request.getMethod()) {
+                    case GET_DEVICE_LIST_METHOD_NAME:
+                        processGetDeviceList(ctx, md);
+                    case SEND_MSG_METHOD_NAME:
+                        processSendMsg(ctx, md, request);
+                        break;
+                    default:
+                        throw new RuleException("Method " + request.getMethod() + " not supported!");
+                }
+            } catch (RuleException e) {
+                throw e;
+            } catch (Exception e) {
+                throw new RuleException(e.getMessage(), e);
+            }
+        }
+    }
+
+    public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+        UUID requestId = msg.getId();
+        PendingRpcRequestMetadata pendindMsg = pendingMsgs.remove(requestId);
+        if (pendindMsg != null) {
+            log.trace("[{}] Received response: {}", requestId, msg);
+            ToServerRpcResponseMsg response;
+            if (msg.getError().isPresent()) {
+                response = new ToServerRpcResponseMsg(pendindMsg.getRequestId(), toJsonString(msg.getError().get()));
+            } else {
+                response = new ToServerRpcResponseMsg(pendindMsg.getRequestId(), msg.getResponse().orElse(""));
+            }
+            ctx.reply(new RpcResponsePluginToRuleMsg(
+                    pendindMsg.getUid(), pendindMsg.getTenantId(), pendindMsg.getRuleId(), response));
+        } else {
+            log.trace("[{}] Received stale response: {}", requestId, msg);
+        }
+    }
+
+    private void processGetDeviceList(PluginContext ctx, PendingRpcRequestMetadata requestMd) {
+        CustomerId customerId = requestMd.getCustomerId();
+        if (!customerId.isNullUid()) {
+            ctx.getCustomerDevices(requestMd.getTenantId(), customerId, configuration.getMaxDeviceCountPerCustomer(), new PluginCallback<List<Device>>() {
+                @Override
+                public void onSuccess(PluginContext ctx, List<Device> devices) {
+                    JsonArray deviceList = new JsonArray();
+                    devices.stream().filter(device -> !requestMd.getDeviceId().equals(device.getId())).forEach(device -> {
+                        JsonObject deviceJson = new JsonObject();
+                        deviceJson.addProperty("id", device.getId().toString());
+                        deviceJson.addProperty("name", device.getName());
+                        deviceList.add(deviceJson);
+                    });
+                    ToServerRpcResponseMsg response = new ToServerRpcResponseMsg(requestMd.getRequestId(), GSON.toJson(deviceList));
+                    ctx.reply(new RpcResponsePluginToRuleMsg(
+                            requestMd.getUid(), requestMd.getTenantId(), requestMd.getRuleId(), response));
+                }
+
+                @Override
+                public void onFailure(PluginContext ctx, Exception e) {
+                    replyWithError(ctx, requestMd, RpcError.INTERNAL);
+                }
+            });
+        } else {
+            replyWithError(ctx, requestMd, "Device is unassigned!");
+        }
+    }
+
+    private void processSendMsg(PluginContext ctx, PendingRpcRequestMetadata requestMd, ToServerRpcRequestMsg request) {
+        JsonObject params = new JsonParser().parse(request.getParams()).getAsJsonObject();
+        String targetDeviceIdStr = params.get(DEVICE_ID).getAsString();
+        DeviceId targetDeviceId = DeviceId.fromString(targetDeviceIdStr);
+        boolean oneWay = isOneWay(params);
+        long timeout = getTimeout(params);
+        if (timeout <= 0) {
+            replyWithError(ctx, requestMd, "Timeout can't be negative!");
+        } else if (timeout > configuration.getMaxTimeout()) {
+            replyWithError(ctx, requestMd, "Timeout is too large!");
+        } else {
+            ctx.getDevice(targetDeviceId, new PluginCallback<Device>() {
+                @Override
+                public void onSuccess(PluginContext ctx, Device targetDevice) {
+                    UUID uid = UUID.randomUUID();
+                    if (targetDevice == null) {
+                        replyWithError(ctx, requestMd, RpcError.NOT_FOUND);
+                    } else if (!requestMd.getCustomerId().isNullUid() &&
+                            requestMd.getTenantId().equals(targetDevice.getTenantId())
+                            && requestMd.getCustomerId().equals(targetDevice.getCustomerId())) {
+                        pendingMsgs.put(uid, requestMd);
+                        log.trace("[{}] Forwarding {} to [{}]", uid, params, targetDeviceId);
+                        ToDeviceRpcRequestBody requestBody = new ToDeviceRpcRequestBody(ON_MSG_METHOD_NAME, GSON.toJson(params.get("body")));
+                        ctx.sendRpcRequest(new ToDeviceRpcRequest(uid, targetDevice.getTenantId(), targetDeviceId, oneWay, System.currentTimeMillis() + timeout, requestBody));
+                    } else {
+                        replyWithError(ctx, requestMd, RpcError.FORBIDDEN);
+                    }
+                }
+
+                @Override
+                public void onFailure(PluginContext ctx, Exception e) {
+                    replyWithError(ctx, requestMd, RpcError.INTERNAL);
+                }
+            });
+        }
+    }
+
+    private boolean isOneWay(JsonObject params) {
+        boolean oneWay = false;
+        if (params.has(ONEWAY)) {
+            oneWay = params.get(ONEWAY).getAsBoolean();
+        }
+        return oneWay;
+    }
+
+    private long getTimeout(JsonObject params) {
+        long timeout;
+        if (params.has(TIMEOUT)) {
+            timeout = params.get(TIMEOUT).getAsLong();
+        } else {
+            timeout = configuration.getDefaultTimeout();
+        }
+        return timeout;
+    }
+
+    private void replyWithError(PluginContext ctx, PendingRpcRequestMetadata requestMd, RpcError error) {
+        replyWithErrorJson(ctx, requestMd, toJsonString(error));
+    }
+
+    private void replyWithError(PluginContext ctx, PendingRpcRequestMetadata requestMd, String error) {
+        replyWithErrorJson(ctx, requestMd, toJsonString(error));
+    }
+
+    private void replyWithErrorJson(PluginContext ctx, PendingRpcRequestMetadata requestMd, String error) {
+        ToServerRpcResponseMsg response = new ToServerRpcResponseMsg(requestMd.getRequestId(), error);
+        ctx.reply(new RpcResponsePluginToRuleMsg(
+                requestMd.getUid(), requestMd.getTenantId(), requestMd.getRuleId(), response));
+    }
+
+    private String toJsonString(String error) {
+        JsonObject errorObj = new JsonObject();
+        errorObj.addProperty("error", error);
+        return GSON.toJson(errorObj);
+    }
+
+    private String toJsonString(RpcError error) {
+        JsonObject errorObj = new JsonObject();
+        switch (error) {
+            case NOT_FOUND:
+                errorObj.addProperty("error", "Target device not found!");
+                break;
+            case NO_ACTIVE_CONNECTION:
+                errorObj.addProperty("error", "No active connection to remote device!");
+                break;
+            case TIMEOUT:
+                errorObj.addProperty("error", "Timeout while waiting response from device!");
+                break;
+            case FORBIDDEN:
+                errorObj.addProperty("error", "This action is not allowed! Devices are unassigned or assigned to different customers!");
+                break;
+            case INTERNAL:
+                errorObj.addProperty("error", "Internal server error!");
+                break;
+        }
+        return GSON.toJson(errorObj);
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java
new file mode 100644
index 0000000..fef27c1
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/messaging/PendingRpcRequestMetadata.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.messaging;
+
+import lombok.Data;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+class PendingRpcRequestMetadata {
+    private final UUID uid;
+    private final int requestId;
+    private final TenantId tenantId;
+    private final RuleId ruleId;
+    private final CustomerId customerId;
+    private final DeviceId deviceId;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java
new file mode 100644
index 0000000..7996823
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/cmd/RpcRequest.java
@@ -0,0 +1,28 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.rpc.cmd;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class RpcRequest {
+    private final String methodName;
+    private final String requestData;
+    private Long timeout;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
new file mode 100644
index 0000000..ba50c32
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/handlers/RpcRestMsgHandler.java
@@ -0,0 +1,127 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.rpc.handlers;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcError;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequestBody;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
+import org.thingsboard.server.extensions.core.plugin.rpc.LocalRequestMetaData;
+import org.thingsboard.server.extensions.core.plugin.rpc.RpcManager;
+import org.thingsboard.server.extensions.core.plugin.rpc.cmd.RpcRequest;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class RpcRestMsgHandler extends DefaultRestMsgHandler {
+
+    private final RpcManager rpcManager;
+    @Setter
+    private long defaultTimeout;
+
+    @Override
+    public void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        boolean valid = false;
+        RestRequest request = msg.getRequest();
+        try {
+            String[] pathParams = request.getPathParams();
+            if (pathParams.length == 2) {
+                String method = pathParams[0].toUpperCase();
+                if (DataConstants.ONEWAY.equals(method) || DataConstants.TWOWAY.equals(method)) {
+                    DeviceId deviceId = DeviceId.fromString(pathParams[1]);
+                    if (!ctx.checkAccess(deviceId)) {
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+                        return;
+                    }
+                    JsonNode rpcRequestBody = jsonMapper.readTree(request.getRequestBody());
+
+                    RpcRequest cmd = new RpcRequest(rpcRequestBody.get("method").asText(),
+                            jsonMapper.writeValueAsString(rpcRequestBody.get("params")));
+                    if (rpcRequestBody.has("timeout")) {
+                        cmd.setTimeout(rpcRequestBody.get("timeout").asLong());
+                    }
+                    long timeout = cmd.getTimeout() != null ? cmd.getTimeout() : defaultTimeout;
+                    ToDeviceRpcRequestBody body = new ToDeviceRpcRequestBody(cmd.getMethodName(), cmd.getRequestData());
+                    ToDeviceRpcRequest rpcRequest = new ToDeviceRpcRequest(UUID.randomUUID(),
+                            ctx.getSecurityCtx().orElseThrow(() -> new IllegalStateException("Security context is empty!")).getTenantId(),
+                            deviceId,
+                            DataConstants.ONEWAY.equals(method),
+                            System.currentTimeMillis() + timeout,
+                            body
+                    );
+                    rpcManager.process(ctx, new LocalRequestMetaData(rpcRequest, msg.getResponseHolder()));
+                    valid = true;
+                }
+            }
+        } catch (IOException e) {
+            log.debug("Failed to process POST request due to IO exception", e);
+        } catch (RuntimeException e) {
+            log.debug("Failed to process POST request due to Runtime exception", e);
+        }
+        if (!valid) {
+            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+        }
+    }
+
+    public void reply(PluginContext ctx, DeferredResult<ResponseEntity> responseWriter, FromDeviceRpcResponse response) {
+        if (response.getError().isPresent()) {
+            RpcError error = response.getError().get();
+            switch (error) {
+                case TIMEOUT:
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
+                    break;
+                case NO_ACTIVE_CONNECTION:
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.CONFLICT));
+                    break;
+                default:
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.REQUEST_TIMEOUT));
+                    break;
+            }
+        } else {
+            if (response.getResponse().isPresent() && !StringUtils.isEmpty(response.getResponse().get())) {
+                String data = response.getResponse().get();
+                try {
+                    responseWriter.setResult(new ResponseEntity<>(jsonMapper.readTree(data), HttpStatus.OK));
+                } catch (IOException e) {
+                    log.debug("Failed to decode device response: {}", data, e);
+                    responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_ACCEPTABLE));
+                }
+            } else {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java
new file mode 100644
index 0000000..96ec7a2
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/LocalRequestMetaData.java
@@ -0,0 +1,30 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.rpc;
+
+import lombok.Data;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.extensions.api.plugins.msg.ToDeviceRpcRequest;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class LocalRequestMetaData {
+    private final ToDeviceRpcRequest request;
+    private final DeferredResult<ResponseEntity> responseWriter;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
new file mode 100644
index 0000000..ccbe16c
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcManager.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.rpc;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.msg.*;
+import org.thingsboard.server.extensions.core.plugin.rpc.handlers.RpcRestMsgHandler;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class RpcManager {
+
+    @Setter
+    private RpcRestMsgHandler restHandler;
+
+    private Map<UUID, LocalRequestMetaData> localRpcRequests = new HashMap<>();
+
+    public void process(PluginContext ctx, LocalRequestMetaData requestMd) {
+        ToDeviceRpcRequest request = requestMd.getRequest();
+        log.trace("[{}] Processing local rpc call for device [{}]", request.getId(), request.getDeviceId());
+        ctx.sendRpcRequest(request);
+        localRpcRequests.put(request.getId(), requestMd);
+        ctx.scheduleTimeoutMsg(new TimeoutUUIDMsg(request.getId(), request.getExpirationTime() - System.currentTimeMillis()));
+    }
+
+    public void process(PluginContext ctx, FromDeviceRpcResponse response) {
+        UUID requestId = response.getId();
+        LocalRequestMetaData md = localRpcRequests.remove(requestId);
+        if (md != null) {
+            log.trace("[{}] Processing local rpc response from device [{}]", requestId, md.getRequest().getDeviceId());
+            restHandler.reply(ctx, md.getResponseWriter(), response);
+        } else {
+            log.trace("[{}] Unknown or stale rpc response received [{}]", requestId, response);
+        }
+    }
+
+    public void process(PluginContext ctx, TimeoutMsg msg) {
+        if (msg instanceof TimeoutUUIDMsg) {
+            UUID requestId = ((TimeoutUUIDMsg) msg).getId();
+            FromDeviceRpcResponse timeoutReponse = new FromDeviceRpcResponse(requestId, null, RpcError.TIMEOUT);
+            LocalRequestMetaData md = localRpcRequests.remove(requestId);
+            if (md != null) {
+                log.trace("[{}] Processing rpc timeout for local device [{}]", requestId, md.getRequest().getDeviceId());
+                restHandler.reply(ctx, md.getResponseWriter(), timeoutReponse);
+            }
+        }
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java
new file mode 100644
index 0000000..ad273d2
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPlugin.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.FromDeviceRpcResponse;
+import org.thingsboard.server.extensions.api.plugins.msg.TimeoutMsg;
+import org.thingsboard.server.extensions.core.plugin.rpc.handlers.RpcRestMsgHandler;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "RPC Plugin", actions = {}, descriptor = "RpcPluginDescriptor.json", configuration = RpcPluginConfiguration.class)
+@Slf4j
+public class RpcPlugin extends AbstractPlugin<RpcPluginConfiguration> {
+
+    private final RpcManager rpcManager;
+    private final RpcRestMsgHandler restMsgHandler;
+
+    public RpcPlugin() {
+        this.rpcManager = new RpcManager();
+        this.restMsgHandler = new RpcRestMsgHandler(rpcManager);
+        this.rpcManager.setRestHandler(restMsgHandler);
+    }
+
+    @Override
+    public void process(PluginContext ctx, FromDeviceRpcResponse msg) {
+        rpcManager.process(ctx, msg);
+    }
+
+    @Override
+    public void process(PluginContext ctx, TimeoutMsg<?> msg) {
+        rpcManager.process(ctx, msg);
+    }
+
+    @Override
+    protected RestMsgHandler getRestMsgHandler() {
+        return restMsgHandler;
+    }
+
+    @Override
+    public void init(RpcPluginConfiguration configuration) {
+        restMsgHandler.setDefaultTimeout(configuration.getDefaultTimeout());
+    }
+
+    @Override
+    public void resume(PluginContext ctx) {
+
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java
new file mode 100644
index 0000000..13db417
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/rpc/RpcPluginConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.rpc;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class RpcPluginConfiguration {
+    private long defaultTimeout;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java
new file mode 100644
index 0000000..1c77ffb
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/AttributeData.java
@@ -0,0 +1,48 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry;
+
+public class AttributeData implements Comparable<AttributeData>{
+
+    private final long lastUpdateTs;
+    private final String key;
+    private final Object value;
+
+    public AttributeData(long lastUpdateTs, String key, Object value) {
+        super();
+        this.lastUpdateTs = lastUpdateTs;
+        this.key = key;
+        this.value = value;
+    }
+
+    public long getLastUpdateTs() {
+        return lastUpdateTs;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public Object getValue() {
+        return value;
+    }
+
+    @Override
+    public int compareTo(AttributeData o) {
+        return Long.compare(lastUpdateTs, o.lastUpdateTs);
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
new file mode 100644
index 0000000..e7bc414
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/AttributesSubscriptionCmd.java
@@ -0,0 +1,36 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
+
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@NoArgsConstructor
+public class AttributesSubscriptionCmd extends SubscriptionCmd {
+
+    public AttributesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe) {
+        super(cmdId, deviceId, keys, unsubscribe);
+    }
+
+    @Override
+    public SubscriptionType getType() {
+        return SubscriptionType.ATTRIBUTES;
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
new file mode 100644
index 0000000..5c9ccc3
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/GetHistoryCmd.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class GetHistoryCmd implements TelemetryPluginCmd {
+
+    private int cmdId;
+    private String deviceId;
+    private String keys;
+    private long startTs;
+    private long endTs;
+
+    @Override
+    public int getCmdId() {
+        return cmdId;
+    }
+
+    @Override
+    public void setCmdId(int cmdId) {
+        this.cmdId = cmdId;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public String getKeys() {
+        return keys;
+    }
+
+    public void setKeys(String keys) {
+        this.keys = keys;
+    }
+
+    public long getStartTs() {
+        return startTs;
+    }
+
+    public void setStartTs(long startTs) {
+        this.startTs = startTs;
+    }
+
+    public long getEndTs() {
+        return endTs;
+    }
+
+    public void setEndTs(long endTs) {
+        this.endTs = endTs;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
new file mode 100644
index 0000000..249dfa9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/SubscriptionCmd.java
@@ -0,0 +1,70 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
+
+import lombok.AllArgsConstructor;
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+@NoArgsConstructor
+@AllArgsConstructor
+public abstract class SubscriptionCmd implements TelemetryPluginCmd {
+
+    private int cmdId;
+    private String deviceId;
+    private String keys;
+    private boolean unsubscribe;
+
+    public abstract SubscriptionType getType();
+
+    public int getCmdId() {
+        return cmdId;
+    }
+
+    public void setCmdId(int cmdId) {
+        this.cmdId = cmdId;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public String getKeys() {
+        return keys;
+    }
+
+    public void setTags(String tags) {
+        this.keys = tags;
+    }
+
+    public boolean isUnsubscribe() {
+        return unsubscribe;
+    }
+
+    public void setUnsubscribe(boolean unsubscribe) {
+        this.unsubscribe = unsubscribe;
+    }
+
+    @Override
+    public String toString() {
+        return "SubscriptionCmd [deviceId=" + deviceId + ", tags=" + keys + ", unsubscribe=" + unsubscribe + "]";
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java
new file mode 100644
index 0000000..cc2febb
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmd.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface TelemetryPluginCmd {
+
+    int getCmdId();
+
+    void setCmdId(int cmdId);
+
+    String getKeys();
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java
new file mode 100644
index 0000000..6d53969
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TelemetryPluginCmdsWrapper.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
+
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class TelemetryPluginCmdsWrapper {
+
+    private List<AttributesSubscriptionCmd> attrSubCmds;
+
+    private List<TimeseriesSubscriptionCmd> tsSubCmds;
+
+    private List<GetHistoryCmd> historyCmds;
+
+    public TelemetryPluginCmdsWrapper() {
+    }
+
+    public List<AttributesSubscriptionCmd> getAttrSubCmds() {
+        return attrSubCmds;
+    }
+
+    public void setAttrSubCmds(List<AttributesSubscriptionCmd> attrSubCmds) {
+        this.attrSubCmds = attrSubCmds;
+    }
+
+    public List<TimeseriesSubscriptionCmd> getTsSubCmds() {
+        return tsSubCmds;
+    }
+
+    public void setTsSubCmds(List<TimeseriesSubscriptionCmd> tsSubCmds) {
+        this.tsSubCmds = tsSubCmds;
+    }
+
+    public List<GetHistoryCmd> getHistoryCmds() {
+        return historyCmds;
+    }
+
+    public void setHistoryCmds(List<GetHistoryCmd> historyCmds) {
+        this.historyCmds = historyCmds;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
new file mode 100644
index 0000000..0b0ff91
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/cmd/TimeseriesSubscriptionCmd.java
@@ -0,0 +1,46 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.cmd;
+
+import lombok.NoArgsConstructor;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+/**
+ * @author Andrew Shvayka
+ */
+@NoArgsConstructor
+public class TimeseriesSubscriptionCmd extends SubscriptionCmd {
+
+    private long timeWindow;
+
+    public TimeseriesSubscriptionCmd(int cmdId, String deviceId, String keys, boolean unsubscribe, long timeWindow) {
+        super(cmdId, deviceId, keys, unsubscribe);
+        this.timeWindow = timeWindow;
+    }
+
+    public long getTimeWindow() {
+        return timeWindow;
+    }
+
+    public void setTimeWindow(long timeWindow) {
+        this.timeWindow = timeWindow;
+    }
+
+    @Override
+    public SubscriptionType getType() {
+        return SubscriptionType.TIMESERIES;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
new file mode 100644
index 0000000..dee981a
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRestMsgHandler.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.rest.PluginRestMsg;
+import org.thingsboard.server.extensions.api.plugins.rest.RestRequest;
+import org.thingsboard.server.extensions.core.plugin.telemetry.AttributeData;
+import org.thingsboard.server.extensions.core.plugin.telemetry.TsData;
+
+import javax.servlet.ServletException;
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class TelemetryRestMsgHandler extends DefaultRestMsgHandler {
+
+    @Override
+    public void handleHttpGetRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        RestRequest request = msg.getRequest();
+        String[] pathParams = request.getPathParams();
+        if (pathParams.length >= 3) {
+            String deviceIdStr = pathParams[0];
+            String method = pathParams[1];
+            String entity = pathParams[2];
+            String scope = pathParams.length >= 4 ? pathParams[3] : null;
+            if (StringUtils.isEmpty(method) || StringUtils.isEmpty(entity) || StringUtils.isEmpty(deviceIdStr)) {
+                msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+                return;
+            }
+
+            DeviceId deviceId = DeviceId.fromString(deviceIdStr);
+
+            if (method.equals("keys")) {
+                if (entity.equals("timeseries")) {
+                    ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
+                        @Override
+                        public void onSuccess(PluginContext ctx, List<TsKvEntry> value) {
+                            List<String> keys = value.stream().map(tsKv -> tsKv.getKey()).collect(Collectors.toList());
+                            msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+                        }
+
+                        @Override
+                        public void onFailure(PluginContext ctx, Exception e) {
+                            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                        }
+                    });
+                } else if (entity.equals("attributes")) {
+                    List<AttributeKvEntry> attributes;
+                    if (!StringUtils.isEmpty(scope)) {
+                        attributes = ctx.loadAttributes(deviceId, scope);
+                    } else {
+                        attributes = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
+                        attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SERVER_SCOPE));
+                        attributes.addAll(ctx.loadAttributes(deviceId, DataConstants.SHARED_SCOPE));
+                    }
+                    List<String> keys = attributes.stream().map(attrKv -> attrKv.getKey()).collect(Collectors.toList());
+                    msg.getResponseHolder().setResult(new ResponseEntity<>(keys, HttpStatus.OK));
+                }
+            } else if (method.equals("values")) {
+                if ("timeseries".equals(entity)) {
+                    String keys = request.getParameter("keys");
+                    Optional<Long> startTs = request.getLongParamValue("startTs");
+                    Optional<Long> endTs = request.getLongParamValue("endTs");
+                    Optional<Integer> limit = request.getIntParamValue("limit");
+                    Map<String, List<TsData>> data = new LinkedHashMap<>();
+                    for (String key : keys.split(",")) {
+                        List<TsKvEntry> entries = ctx.loadTimeseries(deviceId, new BaseTsKvQuery(key, startTs, endTs, limit));
+                        data.put(key, entries.stream().map(v -> new TsData(v.getTs(), v.getValueAsString())).collect(Collectors.toList()));
+                    }
+                    msg.getResponseHolder().setResult(new ResponseEntity<>(data, HttpStatus.OK));
+                } else if ("attributes".equals(entity)) {
+                    String keys = request.getParameter("keys", "");
+                    List<AttributeKvEntry> attributes;
+                    if (!StringUtils.isEmpty(scope)) {
+                        attributes = getAttributeKvEntries(ctx, scope, deviceId, keys);
+                    } else {
+                        attributes = getAttributeKvEntries(ctx, DataConstants.CLIENT_SCOPE, deviceId, keys);
+                        attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SHARED_SCOPE, deviceId, keys));
+                        attributes.addAll(getAttributeKvEntries(ctx, DataConstants.SERVER_SCOPE, deviceId, keys));
+                    }
+                    List<AttributeData> values = attributes.stream().map(attribute -> new AttributeData(attribute.getLastUpdateTs(),
+                            attribute.getKey(), attribute.getValue())).collect(Collectors.toList());
+                    msg.getResponseHolder().setResult(new ResponseEntity<>(values, HttpStatus.OK));
+                }
+            }
+        } else {
+            msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+        }
+    }
+
+    @Override
+    public void handleHttpPostRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        RestRequest request = msg.getRequest();
+        try {
+            String[] pathParams = request.getPathParams();
+            if (pathParams.length == 2) {
+                DeviceId deviceId = DeviceId.fromString(pathParams[0]);
+                String scope = pathParams[1];
+                if (DataConstants.SERVER_SCOPE.equals(scope) ||
+                        DataConstants.SHARED_SCOPE.equals(scope)) {
+                    JsonNode jsonNode = jsonMapper.readTree(request.getRequestBody());
+                    if (jsonNode.isObject()) {
+                        long ts = System.currentTimeMillis();
+                        List<AttributeKvEntry> attributes = new ArrayList<>();
+                        jsonNode.fields().forEachRemaining(entry -> {
+                            String key = entry.getKey();
+                            JsonNode value = entry.getValue();
+                            if (entry.getValue().isTextual()) {
+                                attributes.add(new BaseAttributeKvEntry(new StringDataEntry(key, value.textValue()), ts));
+                            } else if (entry.getValue().isBoolean()) {
+                                attributes.add(new BaseAttributeKvEntry(new BooleanDataEntry(key, value.booleanValue()), ts));
+                            } else if (entry.getValue().isDouble()) {
+                                attributes.add(new BaseAttributeKvEntry(new DoubleDataEntry(key, value.doubleValue()), ts));
+                            } else if (entry.getValue().isNumber()) {
+                                attributes.add(new BaseAttributeKvEntry(new LongDataEntry(key, value.longValue()), ts));
+                            }
+                        });
+                        if (attributes.size() > 0) {
+                            ctx.saveAttributes(deviceId, scope, attributes, new PluginCallback<Void>() {
+                                @Override
+                                public void onSuccess(PluginContext ctx, Void value) {
+                                    msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+                                }
+
+                                @Override
+                                public void onFailure(PluginContext ctx, Exception e) {
+                                    msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR));
+                                }
+                            });
+                            return;
+                        }
+                    }
+                }
+            }
+        } catch (IOException e) {
+            log.debug("Failed to process POST request due to IO exception", e);
+        }
+        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+    }
+
+    @Override
+    public void handleHttpDeleteRequest(PluginContext ctx, PluginRestMsg msg) throws ServletException {
+        RestRequest request = msg.getRequest();
+        try {
+            String[] pathParams = request.getPathParams();
+            if (pathParams.length == 2) {
+                DeviceId deviceId = DeviceId.fromString(pathParams[0]);
+                String scope = pathParams[1];
+                if (DataConstants.SERVER_SCOPE.equals(scope) ||
+                        DataConstants.SHARED_SCOPE.equals(scope)) {
+                    String keysParam = request.getParameter("keys");
+                    if (!StringUtils.isEmpty(keysParam)) {
+                        String[] keys = keysParam.split(",");
+                        ctx.removeAttributes(deviceId, scope, Arrays.asList(keys));
+                        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.OK));
+                        return;
+                    }
+                }
+            }
+        } catch (RuntimeException e) {
+            log.debug("Failed to process DELETE request due to Runtime exception", e);
+        }
+        msg.getResponseHolder().setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+    }
+
+    private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, String scope, DeviceId deviceId, String keysParam) {
+        List<AttributeKvEntry> attributes;
+        if (!StringUtils.isEmpty(keysParam)) {
+            String[] keys = keysParam.split(",");
+            attributes = ctx.loadAttributes(deviceId, scope, Arrays.asList(keys));
+        } else {
+            attributes = ctx.loadAttributes(deviceId, scope);
+        }
+        return attributes;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
new file mode 100644
index 0000000..b166dae
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRpcMsgHandler.java
@@ -0,0 +1,185 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RpcMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.rpc.RpcMsg;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
+import org.thingsboard.server.extensions.core.plugin.telemetry.gen.TelemetryPluginProtos.*;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class TelemetryRpcMsgHandler implements RpcMsgHandler {
+    private final SubscriptionManager subscriptionManager;
+
+    private static final int SUBSCRIPTION_CLAZZ = 1;
+    private static final int SUBSCRIPTION_UPDATE_CLAZZ = 2;
+    private static final int SESSION_CLOSE_CLAZZ = 3;
+    private static final int SUBSCRIPTION_CLOSE_CLAZZ = 4;
+
+    @Override
+    public void process(PluginContext ctx, RpcMsg msg) {
+        switch (msg.getMsgClazz()) {
+            case SUBSCRIPTION_CLAZZ:
+                processSubscriptionCmd(ctx, msg);
+                break;
+            case SUBSCRIPTION_UPDATE_CLAZZ:
+                processRemoteSubscriptionUpdate(ctx, msg);
+                break;
+            case SESSION_CLOSE_CLAZZ:
+                processSessionClose(ctx, msg);
+                break;
+            case SUBSCRIPTION_CLOSE_CLAZZ:
+                processSubscriptionClose(ctx, msg);
+                break;
+            default:
+                throw new RuntimeException("Unknown command id: " + msg.getMsgClazz());
+        }
+    }
+
+    private void processRemoteSubscriptionUpdate(PluginContext ctx, RpcMsg msg) {
+        SubscriptionUpdateProto proto;
+        try {
+            proto = SubscriptionUpdateProto.parseFrom(msg.getMsgData());
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+        subscriptionManager.onRemoteSubscriptionUpdate(ctx, proto.getSessionId(), convert(proto));
+    }
+
+    private void processSubscriptionCmd(PluginContext ctx, RpcMsg msg) {
+        SubscriptionProto proto;
+        try {
+            proto = SubscriptionProto.parseFrom(msg.getMsgData());
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+        Map<String, Long> statesMap = proto.getKeyStatesList().stream().collect(Collectors.toMap(SubscriptionKetStateProto::getKey, SubscriptionKetStateProto::getTs));
+        Subscription subscription = new Subscription(
+                new SubscriptionState(proto.getSessionId(), proto.getSubscriptionId(), DeviceId.fromString(proto.getDeviceId()), SubscriptionType.valueOf(proto.getType()), proto.getAllKeys(), statesMap),
+                false, msg.getServerAddress());
+        subscriptionManager.addRemoteWsSubscription(ctx, msg.getServerAddress(), proto.getSessionId(), subscription);
+    }
+
+    public void onNewSubscription(PluginContext ctx, ServerAddress address, String sessionId, Subscription cmd) {
+        SubscriptionProto.Builder builder = SubscriptionProto.newBuilder();
+        builder.setSessionId(sessionId);
+        builder.setSubscriptionId(cmd.getSubscriptionId());
+        builder.setDeviceId(cmd.getDeviceId().toString());
+        builder.setType(cmd.getType().name());
+        builder.setAllKeys(cmd.isAllKeys());
+        cmd.getKeyStates().entrySet().stream().forEach(e -> builder.addKeyStates(SubscriptionKetStateProto.newBuilder().setKey(e.getKey()).setTs(e.getValue()).build()));
+        ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLAZZ, builder.build().toByteArray()));
+    }
+
+    public void onSubscriptionUpdate(PluginContext ctx, ServerAddress address, String sessionId, SubscriptionUpdate update) {
+        SubscriptionUpdateProto proto = getSubscriptionUpdateProto(sessionId, update);
+        ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_UPDATE_CLAZZ, proto.toByteArray()));
+    }
+
+    public void onSessionClose(PluginContext ctx, ServerAddress address, String vSessionId) {
+        SessionCloseProto proto = SessionCloseProto.newBuilder().setSessionId(vSessionId).build();
+        ctx.sendPluginRpcMsg(new RpcMsg(address, SESSION_CLOSE_CLAZZ, proto.toByteArray()));
+    }
+
+    public void onSubscriptionClose(PluginContext ctx, ServerAddress address, String vSessionId, int subscriptionId) {
+        SubscriptionCloseProto proto = SubscriptionCloseProto.newBuilder().setSessionId(vSessionId).setSubscriptionId(subscriptionId).build();
+        ctx.sendPluginRpcMsg(new RpcMsg(address, SUBSCRIPTION_CLOSE_CLAZZ, proto.toByteArray()));
+    }
+
+    private void processSessionClose(PluginContext ctx, RpcMsg msg) {
+        SessionCloseProto proto;
+        try {
+            proto = SessionCloseProto.parseFrom(msg.getMsgData());
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+        subscriptionManager.cleanupRemoteWsSessionSubscriptions(ctx, proto.getSessionId());
+    }
+
+    private void processSubscriptionClose(PluginContext ctx, RpcMsg msg) {
+        SubscriptionCloseProto proto;
+        try {
+            proto = SubscriptionCloseProto.parseFrom(msg.getMsgData());
+        } catch (InvalidProtocolBufferException e) {
+            throw new RuntimeException(e);
+        }
+        subscriptionManager.removeSubscription(ctx, proto.getSessionId(), proto.getSubscriptionId());
+    }
+
+    private static SubscriptionUpdateProto getSubscriptionUpdateProto(String sessionId, SubscriptionUpdate update) {
+        SubscriptionUpdateProto.Builder builder = SubscriptionUpdateProto.newBuilder();
+        builder.setSessionId(sessionId);
+        builder.setSubscriptionId(update.getSubscriptionId());
+        builder.setErrorCode(update.getErrorCode());
+        if (update.getErrorMsg() != null) {
+            builder.setErrorMsg(update.getErrorMsg());
+        }
+        update.getData().entrySet().stream().forEach(
+                e -> {
+                    SubscriptionUpdateValueListProto.Builder dataBuilder = SubscriptionUpdateValueListProto.newBuilder();
+
+                    dataBuilder.setKey(e.getKey());
+                    e.getValue().forEach(v -> {
+                        Object[] array = (Object[]) v;
+                        dataBuilder.addTs((long) array[0]);
+                        dataBuilder.addValue((String) array[1]);
+                    });
+
+                    builder.addData(dataBuilder.build());
+                }
+        );
+        return builder.build();
+    }
+
+    private SubscriptionUpdate convert(SubscriptionUpdateProto proto) {
+        if (proto.getErrorCode() > 0) {
+            return new SubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg());
+        } else {
+            Map<String, List<Object>> data = new TreeMap<>();
+            proto.getDataList().stream().forEach(v -> {
+                List<Object> values = data.get(v.getKey());
+                if (values == null) {
+                    values = new ArrayList<>();
+                    data.put(v.getKey(), values);
+                }
+                for (int i = 0; i < v.getTsCount(); i++) {
+                    Object[] value = new Object[2];
+                    value[0] = v.getTs(i);
+                    value[1] = v.getValue(i);
+                    values.add(value);
+                }
+            });
+            return new SubscriptionUpdate(proto.getSubscriptionId(), data);
+        }
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
new file mode 100644
index 0000000..f69d17b
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryRuleMsgHandler.java
@@ -0,0 +1,130 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
+
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.data.kv.KvEntry;
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultRuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.GetAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.ResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.TelemetryUploadRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.UpdateAttributesRequestRuleToPluginMsg;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class TelemetryRuleMsgHandler extends DefaultRuleMsgHandler {
+    private final SubscriptionManager subscriptionManager;
+
+    public TelemetryRuleMsgHandler(SubscriptionManager subscriptionManager) {
+        this.subscriptionManager = subscriptionManager;
+    }
+
+    @Override
+    public void handleGetAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, GetAttributesRequestRuleToPluginMsg msg) {
+        GetAttributesRequest request = msg.getPayload();
+
+        List<AttributeKvEntry> clientAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getClientAttributeNames());
+        List<AttributeKvEntry> sharedAttributes = getAttributeKvEntries(ctx, msg.getDeviceId(), DataConstants.SHARED_SCOPE, request.getSharedAttributeNames());
+
+        BasicGetAttributesResponse response = BasicGetAttributesResponse.onSuccess(request.getMsgType(),
+                request.getRequestId(), BasicAttributeKVMsg.from(clientAttributes, sharedAttributes));
+
+        ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
+    }
+
+    private List<AttributeKvEntry> getAttributeKvEntries(PluginContext ctx, DeviceId deviceId, String scope, Set<String> names) {
+        List<AttributeKvEntry> attributes;
+        if (!names.isEmpty()) {
+            attributes = ctx.loadAttributes(deviceId, scope, new ArrayList<>(names));
+        } else {
+            attributes = Collections.emptyList();
+        }
+        return attributes;
+    }
+
+    @Override
+    public void handleTelemetryUploadRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, TelemetryUploadRequestRuleToPluginMsg msg) {
+        TelemetryUploadRequest request = msg.getPayload();
+        List<TsKvEntry> tsKvEntries = new ArrayList<>();
+        for (Map.Entry<Long, List<KvEntry>> entry : request.getData().entrySet()) {
+            for (KvEntry kv : entry.getValue()) {
+                tsKvEntries.add(new BasicTsKvEntry(entry.getKey(), kv));
+            }
+        }
+        ctx.saveTsData(msg.getDeviceId(), tsKvEntries, new PluginCallback<Void>() {
+            @Override
+            public void onSuccess(PluginContext ctx, Void data) {
+                ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onSuccess(request.getMsgType(), request.getRequestId())));
+                subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), SubscriptionType.TIMESERIES, s -> {
+                    List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+                    for (Map.Entry<Long, List<KvEntry>> entry : request.getData().entrySet()) {
+                        for (KvEntry kv : entry.getValue()) {
+                            if (s.isAllKeys() || s.getKeyStates().containsKey((kv.getKey()))) {
+                                subscriptionUpdate.add(new BasicTsKvEntry(entry.getKey(), kv));
+                            }
+                        }
+                    }
+                    return subscriptionUpdate;
+                });
+            }
+
+            @Override
+            public void onFailure(PluginContext ctx, Exception e) {
+                ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
+            }
+        });
+    }
+
+    @Override
+    public void handleUpdateAttributesRequest(PluginContext ctx, TenantId tenantId, RuleId ruleId, UpdateAttributesRequestRuleToPluginMsg msg) {
+        UpdateAttributesRequest request = msg.getPayload();
+        ctx.saveAttributes(msg.getDeviceId(), DataConstants.CLIENT_SCOPE, request.getAttributes().stream().collect(Collectors.toList()),
+                new PluginCallback<Void>() {
+                    @Override
+                    public void onSuccess(PluginContext ctx, Void value) {
+                        ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onSuccess(request.getMsgType(), request.getRequestId())));
+
+                        subscriptionManager.onLocalSubscriptionUpdate(ctx, msg.getDeviceId(), SubscriptionType.ATTRIBUTES, s -> {
+                            List<TsKvEntry> subscriptionUpdate = new ArrayList<TsKvEntry>();
+                            for (AttributeKvEntry kv : request.getAttributes()) {
+                                if (s.isAllKeys() || s.getKeyStates().containsKey(kv.getKey())) {
+                                    subscriptionUpdate.add(new BasicTsKvEntry(kv.getLastUpdateTs(), kv));
+                                }
+                            }
+                            return subscriptionUpdate;
+                        });
+                    }
+
+                    @Override
+                    public void onFailure(PluginContext ctx, Exception e) {
+                        ctx.reply(new ResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, BasicStatusCodeResponse.onError(request.getMsgType(), request.getRequestId(), e)));
+                    }
+                });
+    }
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
new file mode 100644
index 0000000..6ea7489
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/handlers/TelemetryWebsocketMsgHandler.java
@@ -0,0 +1,314 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.handlers;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.extensions.api.plugins.PluginCallback;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.DefaultWebsocketMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.ws.PluginWebsocketSessionRef;
+import org.thingsboard.server.extensions.api.plugins.ws.WsSessionMetaData;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.BinaryPluginWebSocketMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.PluginWebsocketMsg;
+import org.thingsboard.server.extensions.api.plugins.ws.msg.TextPluginWebSocketMsg;
+import org.thingsboard.server.extensions.core.plugin.telemetry.SubscriptionManager;
+import org.thingsboard.server.extensions.core.plugin.telemetry.cmd.*;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionErrorCode;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class TelemetryWebsocketMsgHandler extends DefaultWebsocketMsgHandler {
+
+    private static final int UNKNOWN_SUBSCRIPTION_ID = 0;
+
+    private final SubscriptionManager subscriptionManager;
+
+    public TelemetryWebsocketMsgHandler(SubscriptionManager subscriptionManager) {
+        this.subscriptionManager = subscriptionManager;
+    }
+
+    @Override
+    protected void handleWebSocketMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, PluginWebsocketMsg<?> wsMsg) {
+        try {
+            TelemetryPluginCmdsWrapper cmdsWrapper = null;
+            if (wsMsg instanceof TextPluginWebSocketMsg) {
+                TextPluginWebSocketMsg textMsg = (TextPluginWebSocketMsg) wsMsg;
+                cmdsWrapper = jsonMapper.readValue(textMsg.getPayload(), TelemetryPluginCmdsWrapper.class);
+            } else if (wsMsg instanceof BinaryPluginWebSocketMsg) {
+                throw new IllegalStateException("Not Implemented!");
+                // TODO: add support of BSON here based on
+                // https://github.com/michel-kraemer/bson4jackson
+            }
+            if (cmdsWrapper != null) {
+                if (cmdsWrapper.getAttrSubCmds() != null) {
+                    cmdsWrapper.getAttrSubCmds().forEach(cmd -> handleWsAttributesSubscriptionCmd(ctx, sessionRef, cmd));
+                }
+                if (cmdsWrapper.getTsSubCmds() != null) {
+                    cmdsWrapper.getTsSubCmds().forEach(cmd -> handleWsTimeseriesSubscriptionCmd(ctx, sessionRef, cmd));
+                }
+                if (cmdsWrapper.getHistoryCmds() != null) {
+                    cmdsWrapper.getHistoryCmds().forEach(cmd -> handleWsHistoryCmd(ctx, sessionRef, cmd));
+                }
+            }
+        } catch (IOException e) {
+            log.warn("Failed to decode subscription cmd: {}", e.getMessage(), e);
+            SubscriptionUpdate update = new SubscriptionUpdate(UNKNOWN_SUBSCRIPTION_ID, SubscriptionErrorCode.INTERNAL_ERROR,
+                    "Session meta-data not found!");
+            sendWsMsg(ctx, sessionRef, update);
+        }
+    }
+
+    @Override
+    protected void cleanupWebSocketSession(PluginContext ctx, String sessionId) {
+        subscriptionManager.cleanupLocalWsSessionSubscriptions(ctx, sessionId);
+    }
+
+    private void handleWsAttributesSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, AttributesSubscriptionCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug("[{}] Processing: {}", sessionId, cmd);
+
+        if (validateSessionMetadata(ctx, sessionRef, cmd, sessionId)) {
+            if (cmd.isUnsubscribe()) {
+                unsubscribe(ctx, cmd, sessionId);
+            } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
+                log.debug("[{}] fetching latest attributes ({}) values for device: {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
+                DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+                Optional<Set<String>> keysOptional = getKeys(cmd);
+                SubscriptionState sub;
+                if (keysOptional.isPresent()) {
+                    List<String> keys = new ArrayList<>(keysOptional.get());
+                    List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE, keys);
+                    List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+                    sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+                    Map<String, Long> subState = new HashMap<>(keys.size());
+                    keys.stream().forEach(key -> subState.put(key, 0L));
+                    attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+                    sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, false, subState);
+                } else {
+                    List<AttributeKvEntry> data = ctx.loadAttributes(deviceId, DataConstants.CLIENT_SCOPE);
+                    List<TsKvEntry> attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList());
+                    sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), attributesData));
+
+                    Map<String, Long> subState = new HashMap<>(attributesData.size());
+                    attributesData.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+
+                    sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.ATTRIBUTES, true, subState);
+                }
+                subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+            }
+        }
+    }
+
+    private void handleWsTimeseriesSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, TimeseriesSubscriptionCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        log.debug("[{}] Processing: {}", sessionId, cmd);
+
+        if (validateSessionMetadata(ctx, sessionRef, cmd, sessionId)) {
+            if (cmd.isUnsubscribe()) {
+                unsubscribe(ctx, cmd, sessionId);
+            } else if (validateSubscriptionCmd(ctx, sessionRef, cmd)) {
+                DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+                Optional<Set<String>> keysOptional = getKeys(cmd);
+
+                if (keysOptional.isPresent()) {
+                    long startTs;
+                    if (cmd.getTimeWindow() > 0) {
+                        List<TsKvEntry> data = new ArrayList<>();
+                        List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+                        log.debug("[{}] fetching timeseries data for last {} ms for keys: ({}) for device : {}", sessionId, cmd.getTimeWindow(), cmd.getKeys(), cmd.getDeviceId());
+                        long endTs = System.currentTimeMillis();
+                        startTs = endTs - cmd.getTimeWindow();
+                        for (String key : keys) {
+                            TsKvQuery query = new BaseTsKvQuery(key, startTs, endTs);
+                            data.addAll(ctx.loadTimeseries(deviceId, query));
+                        }
+                        sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+                        Map<String, Long> subState = new HashMap<>(keys.size());
+                        keys.stream().forEach(key -> subState.put(key, startTs));
+                        data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                        SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
+                        subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                    } else {
+                        List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+                        startTs = System.currentTimeMillis();
+                        log.debug("[{}] fetching latest timeseries data for keys: ({}) for device : {}", sessionId, cmd.getKeys(), cmd.getDeviceId());
+                        ctx.loadLatestTimeseries(deviceId, keys, new PluginCallback<List<TsKvEntry>>() {
+                            @Override
+                            public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
+                                sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+
+                                Map<String, Long> subState = new HashMap<>(keys.size());
+                                keys.stream().forEach(key -> subState.put(key, startTs));
+                                data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                                SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, false, subState);
+                                subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                            }
+
+                            @Override
+                            public void onFailure(PluginContext ctx, Exception e) {
+                                SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                                        "Failed to fetch data!");
+                                sendWsMsg(ctx, sessionRef, update);
+                            }
+                        });
+                    }
+                } else {
+                    ctx.loadLatestTimeseries(deviceId, new PluginCallback<List<TsKvEntry>>() {
+                        @Override
+                        public void onSuccess(PluginContext ctx, List<TsKvEntry> data) {
+                            sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+                            Map<String, Long> subState = new HashMap<>(data.size());
+                            data.stream().forEach(v -> subState.put(v.getKey(), v.getTs()));
+                            SubscriptionState sub = new SubscriptionState(sessionId, cmd.getCmdId(), deviceId, SubscriptionType.TIMESERIES, true, subState);
+                            subscriptionManager.addLocalWsSubscription(ctx, sessionId, deviceId, sub);
+                        }
+
+                        @Override
+                        public void onFailure(PluginContext ctx, Exception e) {
+                            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                                    "Failed to fetch data!");
+                            sendWsMsg(ctx, sessionRef, update);
+                        }
+                    });
+                }
+            }
+        }
+    }
+
+    private void handleWsHistoryCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, GetHistoryCmd cmd) {
+        String sessionId = sessionRef.getSessionId();
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD == null) {
+            log.warn("[{}] Session meta data not found. ", sessionId);
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                    "Session meta-data not found!");
+            sendWsMsg(ctx, sessionRef, update);
+            return;
+        }
+        if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Device id is empty!");
+            sendWsMsg(ctx, sessionRef, update);
+            return;
+        }
+        if (cmd.getKeys() == null || cmd.getKeys().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Keys are empty!");
+            sendWsMsg(ctx, sessionRef, update);
+            return;
+        }
+        DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+        if (!ctx.checkAccess(deviceId)) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                    SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+            sendWsMsg(ctx, sessionRef, update);
+            return;
+        }
+        List<String> keys = new ArrayList<>(getKeys(cmd).orElse(Collections.emptySet()));
+        List<TsKvEntry> data = new ArrayList<>();
+        for (String key : keys) {
+            TsKvQuery query = new BaseTsKvQuery(key, cmd.getStartTs(), cmd.getEndTs());
+            data.addAll(ctx.loadTimeseries(deviceId, query));
+        }
+        sendWsMsg(ctx, sessionRef, new SubscriptionUpdate(cmd.getCmdId(), data));
+    }
+
+    private boolean validateSessionMetadata(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd, String sessionId) {
+        WsSessionMetaData sessionMD = wsSessionsMap.get(sessionId);
+        if (sessionMD == null) {
+            log.warn("[{}] Session meta data not found. ", sessionId);
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR,
+                    "Session meta-data not found!");
+            sendWsMsg(ctx, sessionRef, update);
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    private void unsubscribe(PluginContext ctx, SubscriptionCmd cmd, String sessionId) {
+        if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+            cleanupWebSocketSession(ctx, sessionId);
+        } else {
+            subscriptionManager.removeSubscription(ctx, sessionId, cmd.getCmdId());
+        }
+    }
+
+    private boolean validateSubscriptionCmd(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionCmd cmd) {
+        if (cmd.getDeviceId() == null || cmd.getDeviceId().isEmpty()) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
+                    "Device id is empty!");
+            sendWsMsg(ctx, sessionRef, update);
+            return false;
+        }
+        DeviceId deviceId = DeviceId.fromString(cmd.getDeviceId());
+        if (!ctx.checkAccess(deviceId)) {
+            SubscriptionUpdate update = new SubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.UNAUTHORIZED,
+                    SubscriptionErrorCode.UNAUTHORIZED.getDefaultMsg());
+            sendWsMsg(ctx, sessionRef, update);
+            return false;
+        }
+        return true;
+    }
+
+    private void sendWsMsg(PluginContext ctx, PluginWebsocketSessionRef sessionRef, SubscriptionUpdate update) {
+        TextPluginWebSocketMsg reply;
+        try {
+            reply = new TextPluginWebSocketMsg(sessionRef, jsonMapper.writeValueAsString(update));
+            ctx.send(reply);
+        } catch (JsonProcessingException e) {
+            log.warn("[{}] Failed to encode reply: {}", sessionRef.getSessionId(), update, e);
+        } catch (IOException e) {
+            log.warn("[{}] Failed to send reply: {}", sessionRef.getSessionId(), update, e);
+        }
+    }
+
+    public static Optional<Set<String>> getKeys(TelemetryPluginCmd cmd) {
+        if (!StringUtils.isEmpty(cmd.getKeys())) {
+            Set<String> keys = new HashSet<>();
+            for (String key : cmd.getKeys().split(",")) {
+                keys.add(key);
+            }
+            return Optional.of(keys);
+        } else {
+            return Optional.empty();
+        }
+    }
+
+    public void sendWsMsg(PluginContext ctx, String sessionId, SubscriptionUpdate update) {
+        WsSessionMetaData md = wsSessionsMap.get(sessionId);
+        if (md != null) {
+            sendWsMsg(ctx, md.getSessionRef(), update);
+        }
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
new file mode 100644
index 0000000..9359d7b
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/Subscription.java
@@ -0,0 +1,73 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+
+import java.util.Map;
+
+@Data
+@AllArgsConstructor
+public class Subscription {
+
+    private final SubscriptionState sub;
+    private final boolean local;
+    private ServerAddress server;
+
+    public Subscription(SubscriptionState sub, boolean local) {
+        this(sub, local, null);
+    }
+
+    public String getWsSessionId() {
+        return getSub().getWsSessionId();
+    }
+
+    public int getSubscriptionId() {
+        return getSub().getSubscriptionId();
+    }
+
+    public DeviceId getDeviceId() {
+        return getSub().getDeviceId();
+    }
+
+    public SubscriptionType getType() {
+        return getSub().getType();
+    }
+
+    public boolean isAllKeys() {
+        return getSub().isAllKeys();
+    }
+
+    public Map<String, Long> getKeyStates() {
+        return getSub().getKeyStates();
+    }
+
+    public void setKeyState(String key, long ts) {
+        getSub().getKeyStates().put(key, ts);
+    }
+
+    @Override
+    public String toString() {
+        return "Subscription{" +
+                "sub=" + sub +
+                ", local=" + local +
+                ", server=" + server +
+                '}';
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java
new file mode 100644
index 0000000..4f229f9
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionErrorCode.java
@@ -0,0 +1,50 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
+
+public enum SubscriptionErrorCode {
+
+    NO_ERROR(0), INTERNAL_ERROR(1, "Internal Server error!"), BAD_REQUEST(2, "Bad request"), UNAUTHORIZED(3, "Unauthorized");
+
+    private final int code;
+    private final String defaultMsg;
+
+    private SubscriptionErrorCode(int code) {
+        this(code, null);
+    }
+
+    private SubscriptionErrorCode(int code, String defaultMsg) {
+        this.code = code;
+        this.defaultMsg = defaultMsg;
+    }
+
+    public static SubscriptionErrorCode forCode(int code) {
+        for (SubscriptionErrorCode errorCode : SubscriptionErrorCode.values()) {
+            if (errorCode.getCode() == code) {
+                return errorCode;
+            }
+        }
+        throw new IllegalArgumentException("Invalid error code: " + code);
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getDefaultMsg() {
+        return defaultMsg;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
new file mode 100644
index 0000000..8606ff6
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionState.java
@@ -0,0 +1,47 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import org.thingsboard.server.common.data.id.DeviceId;
+
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+@AllArgsConstructor
+public class SubscriptionState {
+
+    private final String wsSessionId;
+    private final int subscriptionId;
+    private final DeviceId deviceId;
+    private final SubscriptionType type;
+    private final boolean allKeys;
+    private final Map<String, Long> keyStates;
+
+    @Override
+    public String toString() {
+        return "SubscriptionState{" +
+                "type=" + type +
+                ", deviceId=" + deviceId +
+                ", subscriptionId=" + subscriptionId +
+                ", wsSessionId='" + wsSessionId + '\'' +
+                '}';
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java
new file mode 100644
index 0000000..7dafde7
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionType.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
+
+/**
+ * @author Andrew Shvayka
+ */
+public enum SubscriptionType {
+    ATTRIBUTES, TIMESERIES
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
new file mode 100644
index 0000000..33aabe1
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/sub/SubscriptionUpdate.java
@@ -0,0 +1,97 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry.sub;
+
+import org.thingsboard.server.common.data.kv.TsKvEntry;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class SubscriptionUpdate {
+
+    private int subscriptionId;
+    private int errorCode;
+    private String errorMsg;
+    private Map<String, List<Object>> data;
+
+    public SubscriptionUpdate(int subscriptionId, List<TsKvEntry> data) {
+        super();
+        this.subscriptionId = subscriptionId;
+        this.data = new TreeMap<>();
+        for (TsKvEntry tsEntry : data) {
+            List<Object> values = this.data.get(tsEntry.getKey());
+            if (values == null) {
+                values = new ArrayList<>();
+                this.data.put(tsEntry.getKey(), values);
+            }
+            Object[] value = new Object[2];
+            value[0] = tsEntry.getTs();
+            value[1] = tsEntry.getValueAsString();
+            values.add(value);
+        }
+    }
+
+    public SubscriptionUpdate(int subscriptionId, Map<String, List<Object>> data) {
+        super();
+        this.subscriptionId = subscriptionId;
+        this.data = data;
+    }
+
+    public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode) {
+        this(subscriptionId, errorCode, null);
+    }
+
+    public SubscriptionUpdate(int subscriptionId, SubscriptionErrorCode errorCode, String errorMsg) {
+        super();
+        this.subscriptionId = subscriptionId;
+        this.errorCode = errorCode.getCode();
+        this.errorMsg = errorMsg != null ? errorMsg : errorCode.getDefaultMsg();
+    }
+
+    public int getSubscriptionId() {
+        return subscriptionId;
+    }
+
+    public Map<String, List<Object>> getData() {
+        return data;
+    }
+
+    public Map<String, Long> getLatestValues() {
+        if (data == null) {
+            return Collections.emptyMap();
+        } else {
+            return data.entrySet().stream().collect(Collectors.toMap(e -> e.getKey(), e -> {
+                List<Object> data = e.getValue();
+                Object[] latest = (Object[]) data.get(data.size() - 1);
+                return (long) latest[0];
+            }));
+        }
+    }
+
+    public int getErrorCode() {
+        return errorCode;
+    }
+
+    public String getErrorMsg() {
+        return errorMsg;
+    }
+
+    @Override
+    public String toString() {
+        return "SubscriptionUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", data="
+                + data + "]";
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
new file mode 100644
index 0000000..637500e
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/SubscriptionManager.java
@@ -0,0 +1,284 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry;
+
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.kv.*;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.Subscription;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionState;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionType;
+import org.thingsboard.server.extensions.core.plugin.telemetry.sub.SubscriptionUpdate;
+
+import java.util.*;
+import java.util.function.Function;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class SubscriptionManager {
+
+    private final Map<DeviceId, Set<Subscription>> subscriptionsByDeviceId = new HashMap<>();
+
+    private final Map<String, Map<Integer, Subscription>> subscriptionsByWsSessionId = new HashMap<>();
+
+    @Setter
+    private TelemetryWebsocketMsgHandler websocketHandler;
+    @Setter
+    private TelemetryRpcMsgHandler rpcHandler;
+
+    public void addLocalWsSubscription(PluginContext ctx, String sessionId, DeviceId deviceId, SubscriptionState sub) {
+        Optional<ServerAddress> server = ctx.resolve(deviceId);
+        Subscription subscription;
+        if (server.isPresent()) {
+            ServerAddress address = server.get();
+            log.trace("[{}] Forwarding subscription [{}] for device [{}] to [{}]", sessionId, sub.getSubscriptionId(), deviceId, address);
+            subscription = new Subscription(sub, true, address);
+            rpcHandler.onNewSubscription(ctx, address, sessionId, subscription);
+        } else {
+            log.trace("[{}] Registering local subscription [{}] for device [{}]", sessionId, sub.getSubscriptionId(), deviceId);
+            subscription = new Subscription(sub, true);
+        }
+        registerSubscription(sessionId, deviceId, subscription);
+    }
+
+    public void addRemoteWsSubscription(PluginContext ctx, ServerAddress address, String sessionId, Subscription subscription) {
+        DeviceId deviceId = subscription.getDeviceId();
+        log.trace("[{}] Registering remote subscription [{}] for device [{}] to [{}]", sessionId, subscription.getSubscriptionId(), deviceId, address);
+        registerSubscription(sessionId, deviceId, subscription);
+        List<TsKvEntry> missedUpdates = new ArrayList<>();
+        if (subscription.getType() == SubscriptionType.ATTRIBUTES) {
+            subscription.getKeyStates().entrySet().stream().forEach(e -> {
+                        Optional<AttributeKvEntry> latestOpt = ctx.loadAttribute(deviceId, DataConstants.CLIENT_SCOPE, e.getKey());
+                        if (latestOpt.isPresent()) {
+                            AttributeKvEntry latestEntry = latestOpt.get();
+                            if (latestEntry.getLastUpdateTs() > e.getValue()) {
+                                missedUpdates.add(new BasicTsKvEntry(latestEntry.getLastUpdateTs(), latestEntry));
+                            }
+                        }
+                    }
+            );
+        } else if (subscription.getType() == SubscriptionType.TIMESERIES) {
+            long curTs = System.currentTimeMillis();
+            subscription.getKeyStates().entrySet().forEach(e -> {
+                TsKvQuery query = new BaseTsKvQuery(e.getKey(), e.getValue() + 1L, curTs);
+                missedUpdates.addAll(ctx.loadTimeseries(deviceId, query));
+            });
+        }
+        if (!missedUpdates.isEmpty()) {
+            rpcHandler.onSubscriptionUpdate(ctx, address, sessionId, new SubscriptionUpdate(subscription.getSubscriptionId(), missedUpdates));
+        }
+    }
+
+    private void registerSubscription(String sessionId, DeviceId deviceId, Subscription subscription) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(subscription.getDeviceId());
+        if (deviceSubscriptions == null) {
+            deviceSubscriptions = new HashSet<>();
+            subscriptionsByDeviceId.put(deviceId, deviceSubscriptions);
+        }
+        deviceSubscriptions.add(subscription);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions == null) {
+            sessionSubscriptions = new HashMap<>();
+            subscriptionsByWsSessionId.put(sessionId, sessionSubscriptions);
+        }
+        sessionSubscriptions.put(subscription.getSubscriptionId(), subscription);
+    }
+
+    public void removeSubscription(PluginContext ctx, String sessionId, Integer subscriptionId) {
+        log.debug("[{}][{}] Going to remove subscription.", sessionId, subscriptionId);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions != null) {
+            Subscription subscription = sessionSubscriptions.remove(subscriptionId);
+            if (subscription != null) {
+                DeviceId deviceId = subscription.getDeviceId();
+                if (subscription.isLocal() && subscription.getServer() != null) {
+                    rpcHandler.onSubscriptionClose(ctx, subscription.getServer(), sessionId, subscription.getSubscriptionId());
+                }
+                if (sessionSubscriptions.isEmpty()) {
+                    log.debug("[{}] Removed last subscription for particular session.", sessionId);
+                    subscriptionsByWsSessionId.remove(sessionId);
+                } else {
+                    log.debug("[{}] Removed session subscription.", sessionId);
+                }
+                Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+                if (deviceSubscriptions != null) {
+                    boolean result = deviceSubscriptions.remove(subscription);
+                    if (result) {
+                        if (deviceSubscriptions.size() == 0) {
+                            log.debug("[{}] Removed last subscription for particular device.", sessionId);
+                            subscriptionsByDeviceId.remove(deviceId);
+                        } else {
+                            log.debug("[{}] Removed device subscription.", sessionId);
+                        }
+                    } else {
+                        log.debug("[{}] Subscription not found!", sessionId);
+                    }
+                } else {
+                    log.debug("[{}] No device subscriptions found!", sessionId);
+                }
+            } else {
+                log.debug("[{}][{}] Subscription not found!", sessionId, subscriptionId);
+            }
+        } else {
+            log.debug("[{}] No session subscriptions found!", sessionId);
+        }
+    }
+
+    public void onLocalSubscriptionUpdate(PluginContext ctx, DeviceId deviceId, SubscriptionType type, Function<Subscription, List<TsKvEntry>> f) {
+        Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+        if (deviceSubscriptions != null) {
+            deviceSubscriptions.stream().filter(s -> type == s.getType()).forEach(s -> {
+                String sessionId = s.getWsSessionId();
+                List<TsKvEntry> subscriptionUpdate = f.apply(s);
+                if (!subscriptionUpdate.isEmpty()) {
+                    SubscriptionUpdate update = new SubscriptionUpdate(s.getSubscriptionId(), subscriptionUpdate);
+                    if (s.isLocal()) {
+                        updateSubscriptionState(sessionId, s, update);
+                        websocketHandler.sendWsMsg(ctx, sessionId, update);
+                    } else {
+                        rpcHandler.onSubscriptionUpdate(ctx, s.getServer(), sessionId, update);
+                    }
+                }
+            });
+        } else {
+            log.debug("[{}] No device subscriptions to process!", deviceId);
+        }
+    }
+
+    public void onRemoteSubscriptionUpdate(PluginContext ctx, String sessionId, SubscriptionUpdate update) {
+        log.trace("[{}] Processing remote subscription onUpdate [{}]", sessionId, update);
+        Optional<Subscription> subOpt = getSubscription(sessionId, update.getSubscriptionId());
+        if (subOpt.isPresent()) {
+            updateSubscriptionState(sessionId, subOpt.get(), update);
+            websocketHandler.sendWsMsg(ctx, sessionId, update);
+        }
+    }
+
+    private void updateSubscriptionState(String sessionId, Subscription subState, SubscriptionUpdate update) {
+        log.trace("[{}] updating subscription state {} using onUpdate {}", sessionId, subState, update);
+        update.getLatestValues().entrySet().forEach(e -> subState.setKeyState(e.getKey(), e.getValue()));
+    }
+
+    private Optional<Subscription> getSubscription(String sessionId, int subscriptionId) {
+        Subscription state = null;
+        Map<Integer, Subscription> subMap = subscriptionsByWsSessionId.get(sessionId);
+        if (subMap != null) {
+            state = subMap.get(subscriptionId);
+        }
+        return Optional.ofNullable(state);
+    }
+
+    public void cleanupLocalWsSessionSubscriptions(PluginContext ctx, String sessionId) {
+        cleanupWsSessionSubscriptions(ctx, sessionId, true);
+    }
+
+    public void cleanupRemoteWsSessionSubscriptions(PluginContext ctx, String sessionId) {
+        cleanupWsSessionSubscriptions(ctx, sessionId, false);
+    }
+
+    private void cleanupWsSessionSubscriptions(PluginContext ctx, String sessionId, boolean localSession) {
+        log.debug("[{}] Removing all subscriptions for particular session.", sessionId);
+        Map<Integer, Subscription> sessionSubscriptions = subscriptionsByWsSessionId.get(sessionId);
+        if (sessionSubscriptions != null) {
+            int sessionSubscriptionSize = sessionSubscriptions.size();
+
+            for (Subscription subscription : sessionSubscriptions.values()) {
+                DeviceId deviceId = subscription.getDeviceId();
+                Set<Subscription> deviceSubscriptions = subscriptionsByDeviceId.get(deviceId);
+                deviceSubscriptions.remove(subscription);
+                if (deviceSubscriptions.isEmpty()) {
+                    subscriptionsByDeviceId.remove(deviceId);
+                }
+            }
+            subscriptionsByWsSessionId.remove(sessionId);
+            log.debug("[{}] Removed {} subscriptions for particular session.", sessionId, sessionSubscriptionSize);
+
+            if (localSession) {
+                Set<ServerAddress> affectedServers = new HashSet<>();
+                for (Subscription subscription : sessionSubscriptions.values()) {
+                    if (subscription.getServer() != null) {
+                        affectedServers.add(subscription.getServer());
+                    }
+                }
+                for (ServerAddress address : affectedServers) {
+                    log.debug("[{}] Going to onSubscriptionUpdate [{}] server about session close event", sessionId, address);
+                    rpcHandler.onSessionClose(ctx, address, sessionId);
+                }
+            }
+        } else {
+            log.debug("[{}] No subscriptions found!", sessionId);
+        }
+    }
+
+    public void onClusterUpdate(PluginContext ctx) {
+        log.trace("Processing cluster onUpdate msg!");
+        Iterator<Map.Entry<DeviceId, Set<Subscription>>> deviceIterator = subscriptionsByDeviceId.entrySet().iterator();
+        while (deviceIterator.hasNext()) {
+            Map.Entry<DeviceId, Set<Subscription>> e = deviceIterator.next();
+            Set<Subscription> subscriptions = e.getValue();
+            Optional<ServerAddress> newAddressOptional = ctx.resolve(e.getKey());
+            if (newAddressOptional.isPresent()) {
+                ServerAddress newAddress = newAddressOptional.get();
+                Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
+                while (subscriptionIterator.hasNext()) {
+                    Subscription s = subscriptionIterator.next();
+                    if (s.isLocal()) {
+                        if (!newAddress.equals(s.getServer())) {
+                            log.trace("[{}] Local subscription is now handled on new server [{}]", s.getWsSessionId(), newAddress);
+                            s.setServer(newAddress);
+                            rpcHandler.onNewSubscription(ctx, newAddress, s.getWsSessionId(), s);
+                        }
+                    } else {
+                        log.trace("[{}] Remote subscription is now handled on new server address: [{}]", s.getWsSessionId(), newAddress);
+                        subscriptionIterator.remove();
+
+                        //TODO: onUpdate state of subscription by WsSessionId and other maps.
+                    }
+                }
+            } else {
+                Iterator<Subscription> subscriptionIterator = subscriptions.iterator();
+                while (subscriptionIterator.hasNext()) {
+                    Subscription s = subscriptionIterator.next();
+                    if (s.isLocal()) {
+                        if (s.getServer() != null) {
+                            log.trace("[{}] Local subscription is no longer handled on remote server address [{}]", s.getWsSessionId(), s.getServer());
+                            s.setServer(null);
+                        }
+                    } else {
+                        log.trace("[{}] Remote subscription is on up to date server address.", s.getWsSessionId());
+                    }
+                }
+            }
+            if (subscriptions.size() == 0) {
+                log.trace("[{}] No more subscriptions for this device on current server.", e.getKey());
+                deviceIterator.remove();
+            }
+        }
+    }
+
+    public void clear() {
+        subscriptionsByWsSessionId.clear();
+        subscriptionsByDeviceId.clear();
+    }
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
new file mode 100644
index 0000000..63d145d
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TelemetryStoragePlugin.java
@@ -0,0 +1,106 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.msg.cluster.ServerAddress;
+import org.thingsboard.server.extensions.api.component.EmptyComponentConfiguration;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RestMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.handlers.RpcMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.handlers.WebsocketMsgHandler;
+import org.thingsboard.server.extensions.core.action.telemetry.TelemetryPluginAction;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRestMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRpcMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryRuleMsgHandler;
+import org.thingsboard.server.extensions.core.plugin.telemetry.handlers.TelemetryWebsocketMsgHandler;
+
+@Plugin(name = "Telemetry Plugin", actions = {TelemetryPluginAction.class})
+@Slf4j
+public class TelemetryStoragePlugin extends AbstractPlugin<EmptyComponentConfiguration> {
+
+    private final TelemetryRestMsgHandler restMsgHandler;
+    private final TelemetryRuleMsgHandler ruleMsgHandler;
+    private final TelemetryWebsocketMsgHandler websocketMsgHandler;
+    private final TelemetryRpcMsgHandler rpcMsgHandler;
+    private final SubscriptionManager subscriptionManager;
+
+    public TelemetryStoragePlugin() {
+        this.subscriptionManager = new SubscriptionManager();
+        this.restMsgHandler = new TelemetryRestMsgHandler();
+        this.ruleMsgHandler = new TelemetryRuleMsgHandler(subscriptionManager);
+        this.websocketMsgHandler = new TelemetryWebsocketMsgHandler(subscriptionManager);
+        this.rpcMsgHandler = new TelemetryRpcMsgHandler(subscriptionManager);
+        this.subscriptionManager.setWebsocketHandler(this.websocketMsgHandler);
+        this.subscriptionManager.setRpcHandler(this.rpcMsgHandler);
+    }
+
+    @Override
+    public void init(EmptyComponentConfiguration configuration) {
+
+    }
+
+    @Override
+    protected RestMsgHandler getRestMsgHandler() {
+        return restMsgHandler;
+    }
+
+    @Override
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return ruleMsgHandler;
+    }
+
+    @Override
+    protected WebsocketMsgHandler getWebsocketMsgHandler() {
+        return websocketMsgHandler;
+    }
+
+    @Override
+    protected RpcMsgHandler getRpcMsgHandler() {
+        return rpcMsgHandler;
+    }
+
+    @Override
+    public void onServerAdded(PluginContext ctx, ServerAddress server) {
+        subscriptionManager.onClusterUpdate(ctx);
+    }
+
+    @Override
+    public void onServerRemoved(PluginContext ctx, ServerAddress server) {
+        subscriptionManager.onClusterUpdate(ctx);
+    }
+
+
+    @Override
+    public void resume(PluginContext ctx) {
+        log.info("Plugin activated!");
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+        log.info("Plugin suspended!");
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+        subscriptionManager.clear();
+        websocketMsgHandler.clear(ctx);
+        log.info("Plugin stopped!");
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java
new file mode 100644
index 0000000..57a1ce2
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/telemetry/TsData.java
@@ -0,0 +1,42 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.telemetry;
+
+public class TsData implements Comparable<TsData>{
+
+    private final long ts;
+    private final String value;
+
+    public TsData(long ts, String value) {
+        super();
+        this.ts = ts;
+        this.value = value;
+    }
+
+    public long getTs() {
+        return ts;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public int compareTo(TsData o) {
+        return Long.compare(ts, o.ts);
+    }
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
new file mode 100644
index 0000000..97fc9ad
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePlugin.java
@@ -0,0 +1,92 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.time;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.id.RuleId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.msg.core.ToServerRpcRequestMsg;
+import org.thingsboard.server.common.msg.core.ToServerRpcResponseMsg;
+import org.thingsboard.server.extensions.api.component.Plugin;
+import org.thingsboard.server.extensions.api.plugins.AbstractPlugin;
+import org.thingsboard.server.extensions.api.plugins.PluginContext;
+import org.thingsboard.server.extensions.api.plugins.handlers.RuleMsgHandler;
+import org.thingsboard.server.extensions.api.plugins.msg.RpcResponsePluginToRuleMsg;
+import org.thingsboard.server.extensions.api.plugins.msg.RuleToPluginMsg;
+import org.thingsboard.server.extensions.api.rules.RuleException;
+import org.thingsboard.server.extensions.core.action.rpc.RpcPluginAction;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Plugin(name = "Time Plugin", actions = {RpcPluginAction.class},
+        descriptor = "TimePluginDescriptor.json", configuration = TimePluginConfiguration.class)
+@Slf4j
+public class TimePlugin extends AbstractPlugin<TimePluginConfiguration> implements RuleMsgHandler {
+
+    private DateTimeFormatter formatter;
+    private String format;
+
+    @Override
+    public void process(PluginContext ctx, TenantId tenantId, RuleId ruleId, RuleToPluginMsg<?> msg) throws RuleException {
+        if (msg.getPayload() instanceof ToServerRpcRequestMsg) {
+            ToServerRpcRequestMsg request = (ToServerRpcRequestMsg) msg.getPayload();
+
+            String reply;
+            if (!StringUtils.isEmpty(format)) {
+                reply = "\"" + formatter.format(LocalDateTime.now()) + "\"";
+            } else {
+                reply = Long.toString(System.currentTimeMillis());
+            }
+            ToServerRpcResponseMsg response = new ToServerRpcResponseMsg(request.getRequestId(), "{\"time\":" + reply + "}");
+            ctx.reply(new RpcResponsePluginToRuleMsg(msg.getUid(), tenantId, ruleId, response));
+        } else {
+            throw new RuntimeException("Not supported msg type: " + msg.getPayload().getClass() + "!");
+        }
+    }
+
+    @Override
+    public void init(TimePluginConfiguration configuration) {
+        format = configuration.getTimeFormat();
+        if (!StringUtils.isEmpty(format)) {
+            formatter = DateTimeFormatter.ofPattern(format);
+        }
+    }
+
+    @Override
+    public void resume(PluginContext ctx) {
+
+    }
+
+    @Override
+    public void suspend(PluginContext ctx) {
+
+    }
+
+    @Override
+    public void stop(PluginContext ctx) {
+
+    }
+
+    @Override
+    protected RuleMsgHandler getRuleMsgHandler() {
+        return this;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java
new file mode 100644
index 0000000..971da74
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/plugin/time/TimePluginConfiguration.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.plugin.time;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class TimePluginConfiguration {
+    private String timeFormat;
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java
new file mode 100644
index 0000000..ea01e45
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessor.java
@@ -0,0 +1,84 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.processor;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.thingsboard.server.common.data.DataConstants;
+import org.thingsboard.server.common.data.Event;
+import org.thingsboard.server.common.msg.device.ToDeviceActorMsg;
+import org.thingsboard.server.extensions.api.component.Processor;
+import org.thingsboard.server.extensions.api.rules.*;
+import org.thingsboard.server.extensions.core.utils.VelocityUtils;
+
+import java.util.Optional;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Processor(name = "Alarm Deduplication Processor", descriptor = "AlarmDeduplicationProcessorDescriptor.json",
+        configuration = AlarmDeduplicationProcessorConfiguration.class)
+@Slf4j
+public class AlarmDeduplicationProcessor extends SimpleRuleLifecycleComponent
+        implements RuleProcessor<AlarmDeduplicationProcessorConfiguration> {
+
+    public static final String IS_NEW_ALARM = "isNewAlarm";
+    private ObjectMapper mapper = new ObjectMapper();
+    private AlarmDeduplicationProcessorConfiguration configuration;
+    private Template alarmIdTemplate;
+    private Template alarmBodyTemplate;
+
+    @Override
+    public void init(AlarmDeduplicationProcessorConfiguration configuration) {
+        this.configuration = configuration;
+        try {
+            this.alarmIdTemplate = VelocityUtils.create(configuration.getAlarmIdTemplate(), "Alarm Id Template");
+            this.alarmBodyTemplate = VelocityUtils.create(configuration.getAlarmBodyTemplate(), "Alarm Body Template");
+        } catch (ParseException e) {
+            log.error("Failed to create templates based on provided configuration!", e);
+            throw new RuntimeException("Failed to create templates based on provided configuration!", e);
+        }
+    }
+
+    @Override
+    public RuleProcessingMetaData process(RuleContext ctx, ToDeviceActorMsg msg) throws RuleException {
+        RuleProcessingMetaData md = new RuleProcessingMetaData();
+        VelocityContext context = VelocityUtils.createContext(ctx.getDeviceAttributes(), msg.getPayload());
+        String alarmId = VelocityUtils.merge(alarmIdTemplate, context);
+        String alarmBody = VelocityUtils.merge(alarmBodyTemplate, context);
+        Optional<Event> existingEvent = ctx.findEvent(DataConstants.ALARM, alarmId);
+        if (!existingEvent.isPresent()) {
+            Event event = new Event();
+            event.setType(DataConstants.ALARM);
+            event.setUid(alarmId);
+            event.setBody(mapper.createObjectNode().put("body", alarmBody));
+            Optional<Event> savedEvent = ctx.saveIfNotExists(event);
+            if (savedEvent.isPresent()) {
+                log.info("New Alarm detected: '{}'", alarmId);
+                md.put(IS_NEW_ALARM, Boolean.TRUE);
+                md.put("alarmId", alarmId);
+                md.put("alarmBody", alarmBody);
+                for (Object key : context.getKeys()) {
+                    md.put(key.toString(), context.get(key.toString()));
+                }
+            }
+        }
+        return md;
+    }
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java
new file mode 100644
index 0000000..fc8becc
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/processor/AlarmDeduplicationProcessorConfiguration.java
@@ -0,0 +1,29 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.processor;
+
+import lombok.Data;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Data
+public class AlarmDeduplicationProcessorConfiguration {
+
+    private String alarmIdTemplate;
+    private String alarmBodyTemplate;
+
+}
diff --git a/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java
new file mode 100644
index 0000000..edbfba1
--- /dev/null
+++ b/extensions-core/src/main/java/org/thingsboard/server/extensions/core/utils/VelocityUtils.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.utils;
+
+import org.apache.velocity.Template;
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.RuntimeSingleton;
+import org.apache.velocity.runtime.parser.ParseException;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
+import org.apache.velocity.tools.generic.DateTool;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
+import org.thingsboard.server.common.msg.core.TelemetryUploadRequest;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.rules.RuleProcessingMetaData;
+import org.thingsboard.server.extensions.core.filter.DeviceAttributesFilter;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class VelocityUtils {
+
+    public static Template create(String source, String templateName) throws ParseException {
+        RuntimeServices runtimeServices = RuntimeSingleton.getRuntimeServices();
+        StringReader reader = new StringReader(source);
+        SimpleNode node = runtimeServices.parse(reader, templateName);
+        Template template = new Template();
+        template.setRuntimeServices(runtimeServices);
+        template.setData(node);
+        template.initDocument();
+        return template;
+    }
+
+    public static String merge(Template template, VelocityContext context) {
+        StringWriter writer = new StringWriter();
+        template.merge(context, writer);
+        return writer.toString();
+    }
+
+    public static VelocityContext createContext(RuleProcessingMetaData metadata) {
+        VelocityContext context = new VelocityContext();
+        metadata.getValues().forEach((k, v) -> context.put(k, v));
+        return context;
+    }
+
+    public static VelocityContext createContext(DeviceAttributes deviceAttributes, FromDeviceMsg payload) {
+        VelocityContext context = new VelocityContext();
+        context.put("date", new DateTool());
+        pushAttributes(context, deviceAttributes.getClientSideAttributes(), DeviceAttributesFilter.CLIENT_SIDE);
+        pushAttributes(context, deviceAttributes.getServerSideAttributes(), DeviceAttributesFilter.SERVER_SIDE);
+        pushAttributes(context, deviceAttributes.getServerSidePublicAttributes(), DeviceAttributesFilter.SHARED);
+
+        switch (payload.getMsgType()) {
+            case POST_TELEMETRY_REQUEST:
+                pushTsEntries(context, (TelemetryUploadRequest) payload);
+                break;
+        }
+
+        return context;
+    }
+
+    private static void pushTsEntries(VelocityContext context, TelemetryUploadRequest payload) {
+        payload.getData().forEach((k, vList) -> {
+            vList.forEach(v -> {
+                context.put(v.getKey(), new BasicTsKvEntry(k, v));
+            });
+        });
+    }
+
+    private static void pushAttributes(VelocityContext context, Collection<AttributeKvEntry> deviceAttributes, String prefix) {
+        Map<String, String> values = new HashMap<>();
+        deviceAttributes.forEach(v -> values.put(v.getKey(), v.getValueAsString()));
+        context.put(prefix, values);
+    }
+}
diff --git a/extensions-core/src/main/proto/telemetry.proto b/extensions-core/src/main/proto/telemetry.proto
new file mode 100644
index 0000000..5c7d7a4
--- /dev/null
+++ b/extensions-core/src/main/proto/telemetry.proto
@@ -0,0 +1,57 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+syntax = "proto3";
+package telemetry;
+
+option java_package = "org.thingsboard.server.extensions.core.plugin.telemetry.gen";
+option java_outer_classname = "TelemetryPluginProtos";
+
+message SubscriptionProto {
+  string sessionId = 1;
+  int32 subscriptionId = 2;
+  string deviceId = 3;
+  string type = 4;
+  bool allKeys = 5;
+  repeated SubscriptionKetStateProto keyStates = 6;
+}
+
+message SubscriptionUpdateProto {
+    string sessionId = 1;
+    int32 subscriptionId = 2;
+    int32 errorCode = 3;
+    string errorMsg = 4;
+    repeated SubscriptionUpdateValueListProto data = 5;
+}
+
+message SessionCloseProto {
+    string sessionId = 1;
+}
+
+message SubscriptionCloseProto {
+    string sessionId = 1;
+    int32 subscriptionId = 2;
+}
+
+message SubscriptionKetStateProto {
+    string key = 1;
+    int64 ts = 2;
+}
+
+message SubscriptionUpdateValueListProto {
+    string key = 1;
+    repeated int64 ts = 2;
+    repeated string value = 3;
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json b/extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json
new file mode 100644
index 0000000..c082fc8
--- /dev/null
+++ b/extensions-core/src/main/resources/AlarmDeduplicationProcessorDescriptor.json
@@ -0,0 +1,30 @@
+{
+  "schema": {
+    "title": "Send Mail Action Configuration",
+    "type": "object",
+    "properties": {
+      "alarmIdTemplate": {
+        "title": "Alarm Id template",
+        "type": "string",
+        "default": "TODO"
+      },
+      "alarmBodyTemplate": {
+        "title": "Alarm body template",
+        "type": "string",
+        "default": "TODO"
+      }
+    },
+    "required": [
+      "alarmIdTemplate",
+      "alarmBodyTemplate"
+    ]
+  },
+  "form": [
+    "alarmIdTemplate",
+    {
+      "key": "alarmBodyTemplate",
+      "type": "textarea",
+      "rows": 2
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json b/extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json
new file mode 100644
index 0000000..5f6740d
--- /dev/null
+++ b/extensions-core/src/main/resources/DeviceMessagingPluginDescriptor.json
@@ -0,0 +1,37 @@
+{
+  "schema": {
+    "title": "Device Messaging configuration",
+    "type": "object",
+    "properties": {
+      "maxDeviceCountPerCustomer": {
+        "title": "Maximum amount of devices per customer",
+        "type": "integer",
+        "default": 1024,
+        "minimum": 0,
+        "maximum": 1024
+      },
+      "defaultTimeout": {
+        "title": "Default request timeout",
+        "type": "integer",
+        "default": 20000,
+        "minimum": 0
+      },
+      "maxTimeout": {
+        "title": "Maximum request timeout",
+        "type": "integer",
+        "default": 60000,
+        "minimum": 0
+      }
+    },
+    "required": [
+      "maxDeviceCountPerCustomer",
+      "defaultTimeout",
+      "maxTimeout"
+    ]
+  },
+  "form": [
+    "maxDeviceCountPerCustomer",
+    "defaultTimeout",
+    "maxTimeout"
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/JsFilterDescriptor.json b/extensions-core/src/main/resources/JsFilterDescriptor.json
new file mode 100644
index 0000000..29624ed
--- /dev/null
+++ b/extensions-core/src/main/resources/JsFilterDescriptor.json
@@ -0,0 +1,21 @@
+{
+  "schema": {
+    "title": "Filter Configuration",
+    "type": "object",
+    "properties": {
+      "filter": {
+        "title": "Filter",
+        "type": "string"
+      }
+    },
+    "required": [
+      "filter"
+    ]
+  },
+  "form": [
+    {
+      "key": "filter",
+      "type": "javascript"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MailPluginData.json b/extensions-core/src/main/resources/MailPluginData.json
new file mode 100644
index 0000000..8adae62
--- /dev/null
+++ b/extensions-core/src/main/resources/MailPluginData.json
@@ -0,0 +1,16 @@
+{
+  "host": "smtp.ukr.net",
+  "port": 465,
+  "username": "thingsboard@ukr.net",
+  "password": "thingsboard123",
+  "otherProperties": [
+    {
+      "key":"mailsmtp.auth",
+      "value":"true"
+    },
+    {
+      "key":"mail.smtp.starttls.enable",
+      "value":"true"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MailPluginDescriptor.json b/extensions-core/src/main/resources/MailPluginDescriptor.json
new file mode 100644
index 0000000..d2291e0
--- /dev/null
+++ b/extensions-core/src/main/resources/MailPluginDescriptor.json
@@ -0,0 +1,61 @@
+{
+  "schema": {
+    "title": "Mail Plugin Configuration",
+    "type": "object",
+    "properties": {
+      "host": {
+        "title": "Mail server host",
+        "type": "string"
+      },
+      "port": {
+        "title": "Mail server port",
+        "type": "integer",
+        "default": 25,
+        "minimum": 0,
+        "maximum": 65536
+      },
+      "username": {
+        "title": "Username",
+        "type": "string"
+      },
+      "password": {
+        "title": "Password",
+        "type": "string"
+      },
+      "otherProperties": {
+        "title": "Other mail properties",
+        "type": "array",
+        "items": {
+          "title": "Mail property",
+          "type": "object",
+          "properties": {
+            "key": {
+              "title": "Key",
+              "type": "string"
+            },
+            "value": {
+              "title": "Value",
+              "type": "string"
+            }
+          }
+        }
+      }
+    },
+    "required": [
+      "host",
+      "port",
+      "username",
+      "password"
+    ]
+  },
+  "form": [
+    "host",
+    "port",
+    "username",
+    {
+      "key": "password",
+      "type": "password"
+    },
+    "otherProperties"
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MethodNameFilterDescriptor.json b/extensions-core/src/main/resources/MethodNameFilterDescriptor.json
new file mode 100644
index 0000000..fe1ac7b
--- /dev/null
+++ b/extensions-core/src/main/resources/MethodNameFilterDescriptor.json
@@ -0,0 +1,28 @@
+{
+  "schema": {
+    "title": "Method Name Filter Configuration",
+    "type": "object",
+    "properties": {
+      "methodNames": {
+        "title": "Method names",
+        "type": "array",
+        "minItems" : 1,
+        "items": {
+          "type": "object",
+          "title": "Method name",
+          "properties": {
+            "name": {
+              "title": "Method Name",
+              "type": "string"
+            }
+          }
+        },
+        "uniqueItems": true
+      }
+    },
+    "required": ["methodNames"]
+  },
+  "form": [
+    "methodNames"
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/MsgTypeFilterDescriptor.json b/extensions-core/src/main/resources/MsgTypeFilterDescriptor.json
new file mode 100644
index 0000000..a0339c9
--- /dev/null
+++ b/extensions-core/src/main/resources/MsgTypeFilterDescriptor.json
@@ -0,0 +1,40 @@
+{
+  "schema": {
+    "title": "Message Type Filter Configuration",
+    "type": "object",
+    "properties": {
+      "messageTypes": {
+        "title": "Message types",
+        "type": "array",
+        "minItems" : 1,
+        "items": [
+          {
+            "value": "GET_ATTRIBUTES",
+            "label": "Get attributes"
+          },
+          {
+            "value": "POST_ATTRIBUTES",
+            "label": "Post attributes"
+          },
+          {
+            "value": "POST_TELEMETRY",
+            "label": "Post telemetry"
+          },
+          {
+            "value": "RPC_REQUEST",
+            "label": "RPC Request"
+          }
+        ],
+        "uniqueItems": true
+      }
+    },
+    "required": ["messageTypes"]
+  },
+  "form": [
+    {
+      "key": "messageTypes",
+      "type": "rc-select",
+      "multiple": true
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/RpcPluginData.json b/extensions-core/src/main/resources/RpcPluginData.json
new file mode 100644
index 0000000..454e33a
--- /dev/null
+++ b/extensions-core/src/main/resources/RpcPluginData.json
@@ -0,0 +1,3 @@
+{
+  "defaultTimeout": 60000
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/RpcPluginDescriptor.json b/extensions-core/src/main/resources/RpcPluginDescriptor.json
new file mode 100644
index 0000000..1e84b26
--- /dev/null
+++ b/extensions-core/src/main/resources/RpcPluginDescriptor.json
@@ -0,0 +1,18 @@
+{
+  "schema": {
+    "title": "RPC Plugin Configuration",
+    "type": "object",
+    "properties": {
+      "defaultTimeout": {
+        "title": "Default timeout",
+        "type": "number"
+      }
+    },
+    "required": [
+      "defaultTimeout"
+    ]
+  },
+  "form": [
+    "*"
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/SendMailActionData.json b/extensions-core/src/main/resources/SendMailActionData.json
new file mode 100644
index 0000000..d9ad53d
--- /dev/null
+++ b/extensions-core/src/main/resources/SendMailActionData.json
@@ -0,0 +1,7 @@
+{
+  "sendFlag": "isNewAlarm",
+  "fromTemplate": "thingsboard@ukr.net",
+  "toTemplate": "andrew.shvayka@gmail.com",
+  "subjectTemplate": "Thingsboard",
+  "bodyTemplate": "Hello World!"
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/SendMailActionDescriptor.json b/extensions-core/src/main/resources/SendMailActionDescriptor.json
new file mode 100644
index 0000000..ed8cdf4
--- /dev/null
+++ b/extensions-core/src/main/resources/SendMailActionDescriptor.json
@@ -0,0 +1,55 @@
+{
+  "schema": {
+    "title": "Send Mail Action Configuration",
+    "type": "object",
+    "properties": {
+      "sendFlag": {
+        "title": "Send flag",
+        "type": "string"
+      },
+      "fromTemplate": {
+        "title": "From template",
+        "type": "string"
+      },
+      "toTemplate": {
+        "title": "To template",
+        "type": "string"
+      },
+      "ccTemplate": {
+        "title": "CC template",
+        "type": "string"
+      },
+      "bccTemplate": {
+        "title": "BCC template",
+        "type": "string"
+      },
+      "subjectTemplate": {
+        "title": "Subject template",
+        "type": "string"
+      },
+      "bodyTemplate": {
+        "title": "Body template",
+        "type": "string"
+      }
+    },
+    "required": [
+      "fromTemplate",
+      "toTemplate",
+      "subjectTemplate",
+      "bodyTemplate"
+    ]
+  },
+  "form": [
+    "sendFlag",
+    "fromTemplate",
+    "toTemplate",
+    "ccTemplate",
+    "bccTemplate",
+    "subjectTemplate",
+    {
+      "key": "bodyTemplate",
+      "type": "textarea",
+      "rows": 5
+    }
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/main/resources/TimePluginDescriptor.json b/extensions-core/src/main/resources/TimePluginDescriptor.json
new file mode 100644
index 0000000..d357958
--- /dev/null
+++ b/extensions-core/src/main/resources/TimePluginDescriptor.json
@@ -0,0 +1,16 @@
+{
+  "schema": {
+    "title": "Time Plugin Configuration",
+    "type": "object",
+    "properties": {
+      "timeFormat": {
+        "title": "Time format",
+        "type": "string",
+        "default": "yyyy MM dd HH:mm:ss.SSS"
+      }
+    }
+  },
+  "form": [
+    "timeFormat"
+  ]
+}
\ No newline at end of file
diff --git a/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
new file mode 100644
index 0000000..3095f5d
--- /dev/null
+++ b/extensions-core/src/test/java/org/thingsboard/server/extensions/core/filter/DeviceAttributesFilterTest.java
@@ -0,0 +1,126 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.extensions.core.filter;
+
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BooleanDataEntry;
+import org.thingsboard.server.common.data.kv.DoubleDataEntry;
+import org.thingsboard.server.extensions.api.device.DeviceAttributes;
+import org.thingsboard.server.extensions.api.rules.RuleContext;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.runners.MockitoJUnitRunner;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class DeviceAttributesFilterTest {
+
+    @Mock
+    RuleContext ruleCtx;
+
+    private static JsFilterConfiguration wrap(String filterBody) {
+        return new JsFilterConfiguration(filterBody);
+    }
+
+    @Test
+    public void basicMissingAttributesTest() {
+        DeviceAttributesFilter filter = new DeviceAttributesFilter();
+        filter.init(wrap("((typeof nonExistingVal === 'undefined') || nonExistingVal == true) && booleanValue == false"));
+        List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+        clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+        DeviceAttributes attributes = new DeviceAttributes(clientAttributes, new ArrayList<>(), new ArrayList<>());
+
+        Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+        Assert.assertTrue(filter.filter(ruleCtx, null));
+        filter.stop();
+    }
+
+    @Test
+    public void basicClientAttributesTest() {
+        DeviceAttributesFilter filter = new DeviceAttributesFilter();
+        filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
+        List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+        clientAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+        clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+        DeviceAttributes attributes = new DeviceAttributes(clientAttributes, new ArrayList<>(), new ArrayList<>());
+
+        Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+        Assert.assertTrue(filter.filter(ruleCtx, null));
+        filter.stop();
+    }
+
+    @Test(timeout = 10000)
+    public void basicClientAttributesStressTest() {
+        DeviceAttributesFilter filter = new DeviceAttributesFilter();
+        filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
+
+        List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+        clientAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+        clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+        DeviceAttributes attributes = new DeviceAttributes(clientAttributes, new ArrayList<>(), new ArrayList<>());
+
+        Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+
+        for (int i = 0; i < 10000; i++) {
+            Assert.assertTrue(filter.filter(ruleCtx, null));
+        }
+        filter.stop();
+    }
+
+    @Test
+    public void basicServerAttributesTest() {
+        DeviceAttributesFilter filter = new DeviceAttributesFilter();
+        filter.init(wrap("doubleValue == 1.0 && booleanValue == false"));
+
+        List<AttributeKvEntry> serverAttributes = new ArrayList<>();
+        serverAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+        serverAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+        DeviceAttributes attributes = new DeviceAttributes(new ArrayList<>(), serverAttributes, new ArrayList<>());
+
+        Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+        Assert.assertTrue(filter.filter(ruleCtx, null));
+        filter.stop();
+    }
+
+    @Test
+    public void basicConflictServerAttributesTest() {
+        DeviceAttributesFilter filter = new DeviceAttributesFilter();
+        filter.init(wrap("cs.doubleValue == 1.0 && cs.booleanValue == true && ss.doubleValue == 0.0 && ss.booleanValue == false"));
+
+        List<AttributeKvEntry> clientAttributes = new ArrayList<>();
+        clientAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 1.0), 42));
+        clientAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", true), 42));
+
+        List<AttributeKvEntry> serverAttributes = new ArrayList<>();
+        serverAttributes.add(new BaseAttributeKvEntry(new DoubleDataEntry("doubleValue", 0.0), 42));
+        serverAttributes.add(new BaseAttributeKvEntry(new BooleanDataEntry("booleanValue", false), 42));
+        DeviceAttributes attributes = new DeviceAttributes(clientAttributes, serverAttributes, new ArrayList<>());
+
+        Mockito.when(ruleCtx.getDeviceAttributes()).thenReturn(attributes);
+        Assert.assertTrue(filter.filter(ruleCtx, null));
+        filter.stop();
+    }
+
+}

LICENSE 201(+201 -0)

diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..c8f142f
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright 2016 The Thingsboard Authors
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/license-header-template.txt b/license-header-template.txt
new file mode 100644
index 0000000..fd245c1
--- /dev/null
+++ b/license-header-template.txt
@@ -0,0 +1,13 @@
+Copyright © ${project.inceptionYear} ${owner}
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

pom.xml 710(+710 -0)

diff --git a/pom.xml b/pom.xml
new file mode 100755
index 0000000..bb5dab2
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,710 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.thingsboard</groupId>
+    <artifactId>server</artifactId>
+    <version>0.0.1-SNAPSHOT</version>
+    <packaging>pom</packaging>
+
+    <name>Thingsboard Server Components</name>
+    <url>http://thingsboard.org</url>
+    <inceptionYear>2016</inceptionYear>
+
+    <properties>
+        <main.dir>${basedir}</main.dir>
+        <spring-boot.version>1.4.2.RELEASE</spring-boot.version>
+        <spring.version>4.3.4.RELEASE</spring.version>
+        <spring-security.version>4.2.0.RELEASE</spring-security.version>
+        <jjwt.version>0.7.0</jjwt.version>
+        <joda-time.version>2.4</joda-time.version>
+        <json-path.version>2.2.0</json-path.version>
+        <junit.version>4.12</junit.version>
+        <slf4j.version>1.7.7</slf4j.version>
+        <logback.version>1.1.7</logback.version>
+        <mockito.version>1.9.5</mockito.version>
+        <rat.version>0.10</rat.version>
+        <cassandra.version>3.0.0</cassandra.version>
+        <cassandra-unit.version>3.0.0.1</cassandra-unit.version>
+        <takari-cpsuite.version>1.2.7</takari-cpsuite.version>
+        <guava.version>18.0</guava.version>
+        <commons-lang3.version>3.4</commons-lang3.version>
+        <commons-validator.version>1.5.0</commons-validator.version>
+        <jackson.version>2.7.3</jackson.version>
+        <json-schema-validator.version>2.2.6</json-schema-validator.version>
+        <scala.version>2.11</scala.version>
+        <akka.version>2.4.2</akka.version>
+        <californium.version>1.0.2</californium.version>
+        <gson.version>2.6.2</gson.version>
+        <velocity.version>1.7</velocity.version>
+        <velocity-tools.version>2.0</velocity-tools.version>
+        <mail.version>1.4.3</mail.version>
+        <curator.version>2.11.0</curator.version>
+        <protobuf.version>3.0.2</protobuf.version>
+        <grpc.version>1.0.0</grpc.version>
+        <lombok.version>1.16.10</lombok.version>
+        <paho.client.version>1.1.0</paho.client.version>
+        <netty.version>4.1.3.Final</netty.version>
+        <os-maven-plugin.version>1.5.0</os-maven-plugin.version>
+        <rabbitmq.version>3.6.5</rabbitmq.version>
+        <kafka.version>0.9.0.0</kafka.version>
+        <hazelcast.version>3.6.6</hazelcast.version>
+        <hazelcast-zookeeper.version>3.6.1</hazelcast-zookeeper.version>
+        <surfire.version>2.19.1</surfire.version>
+        <jar-plugin.version>3.0.2</jar-plugin.version>
+    </properties>
+
+    <modules>
+        <module>common</module>
+        <module>dao</module>
+        <module>extensions-api</module>
+        <module>extensions-core</module>
+        <module>extensions</module>
+        <module>transport</module>
+        <module>ui</module>
+        <module>tools</module>
+        <module>application</module>
+    </modules>
+
+    <profiles>
+        <profile>
+            <id>default</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+        </profile>
+    </profiles>
+
+    <build>
+        <extensions>
+            <extension>
+                <groupId>kr.motd.maven</groupId>
+                <artifactId>os-maven-plugin</artifactId>
+                <version>1.5.0.Final</version>
+            </extension>
+        </extensions>
+        <pluginManagement>
+            <plugins>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-compiler-plugin</artifactId>
+                    <version>2.5.1</version>
+                    <configuration>
+                        <source>1.8</source>
+                        <target>1.8</target>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-resources-plugin</artifactId>
+                    <version>2.7</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-source-plugin</artifactId>
+                    <version>2.2.1</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-jar-plugin</artifactId>
+                    <version>3.0.2</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-maven-plugin</artifactId>
+                    <version>${spring-boot.version}</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.fortasoft</groupId>
+                    <artifactId>gradle-maven-plugin</artifactId>
+                    <version>1.0.8</version>
+                </plugin>
+                <plugin>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-dependency-plugin</artifactId>
+                    <executions>
+                        <execution>
+                            <id>copy-protoc</id>
+                            <phase>generate-sources</phase>
+                            <goals>
+                                <goal>copy</goal>
+                            </goals>
+                            <configuration>
+                                <artifactItems>
+                                    <artifactItem>
+                                        <groupId>com.google.protobuf</groupId>
+                                        <artifactId>protoc</artifactId>
+                                        <version>${protobuf.version}</version>
+                                        <classifier>${os.detected.classifier}</classifier>
+                                        <type>exe</type>
+                                        <overWrite>true</overWrite>
+                                        <outputDirectory>${project.build.directory}</outputDirectory>
+                                    </artifactItem>
+                                </artifactItems>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
+                    <groupId>org.xolstice.maven.plugins</groupId>
+                    <artifactId>protobuf-maven-plugin</artifactId>
+                    <version>0.5.0</version>
+                    <configuration>
+                        <!--
+                          The version of protoc must match protobuf-java. If you don't depend on
+                          protobuf-java directly, you will be transitively depending on the
+                          protobuf-java version that grpc depends on.
+                        -->
+                        <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
+                        </protocArtifact>
+                        <pluginId>grpc-java</pluginId>
+                        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.0.0:exe:${os.detected.classifier}
+                        </pluginArtifact>
+                    </configuration>
+                    <executions>
+                        <execution>
+                            <goals>
+                                <goal>compile</goal>
+                                <goal>compile-custom</goal>
+                                <goal>test-compile</goal>
+                            </goals>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>build-helper-maven-plugin</artifactId>
+                    <version>1.12</version>
+                    <executions>
+                        <execution>
+                            <id>add-source</id>
+                            <phase>generate-sources</phase>
+                            <goals>
+                                <goal>add-source</goal>
+                            </goals>
+                            <configuration>
+                                <sources>
+                                    <source>${basedir}/target/generated-sources</source>
+                                </sources>
+                            </configuration>
+                        </execution>
+                    </executions>
+                </plugin>
+                <plugin>
+                    <groupId>org.eclipse.m2e</groupId>
+                    <artifactId>lifecycle-mapping</artifactId>
+                    <version>1.0.0</version>
+                    <configuration>
+                        <lifecycleMappingMetadata>
+                            <pluginExecutions>
+                                <pluginExecution>
+                                    <pluginExecutionFilter>
+                                        <groupId>
+                                            org.apache.maven.plugins
+                                        </groupId>
+                                        <artifactId>
+                                            maven-antrun-plugin
+                                        </artifactId>
+                                        <versionRange>
+                                            [1.3,)
+                                        </versionRange>
+                                        <goals>
+                                            <goal>run</goal>
+                                        </goals>
+                                    </pluginExecutionFilter>
+                                    <action>
+                                        <ignore></ignore>
+                                    </action>
+                                </pluginExecution>
+                            </pluginExecutions>
+                        </lifecycleMappingMetadata>
+                    </configuration>
+                </plugin>
+                <plugin>
+                    <groupId>com.mycila</groupId>
+                    <artifactId>license-maven-plugin</artifactId>
+                    <version>3.0</version>
+                    <configuration>
+                        <header>${main.dir}/license-header-template.txt</header>
+                        <properties>
+                            <owner>The Thingsboard Authors</owner>
+                        </properties>
+                        <excludes>
+                            <exclude>**/.env</exclude>
+                            <exclude>**/.eslintrc</exclude>
+                            <exclude>**/.babelrc</exclude>
+                            <exclude>**/.jshintrc</exclude>
+                            <exclude>**/.gradle/**</exclude>
+                            <exclude>**/nightwatch</exclude>
+                            <exclude>**/README</exclude>
+                            <exclude>**/LICENSE</exclude>
+                            <exclude>**/banner.txt</exclude>
+                            <exclude>node_modules/**</exclude>
+                            <exclude>**/*.properties</exclude>
+                            <exclude>src/test/resources/**</exclude>
+                            <exclude>src/vendor/**</exclude>
+                            <exclude>src/font/**</exclude>
+                            <exclude>src/sh/**</exclude>
+                            <exclude>src/main/scripts/control/**</exclude>
+                        </excludes>
+                        <mapping>
+                            <proto>JAVADOC_STYLE</proto>
+                            <cql>DOUBLEDASHES_STYLE</cql>
+                            <scss>JAVADOC_STYLE</scss>
+                            <jsx>SLASHSTAR_STYLE</jsx>
+                            <conf>SCRIPT_STYLE</conf>
+                            <gradle>JAVADOC_STYLE</gradle>
+                        </mapping>
+                    </configuration>
+                    <executions>
+                        <execution>
+                            <goals>
+                                <goal>check</goal>
+                            </goals>
+                        </execution>
+                    </executions>
+                </plugin>
+            </plugins>
+        </pluginManagement>
+        <plugins>
+            <plugin>
+                <groupId>com.mycila</groupId>
+                <artifactId>license-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
+
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.thingsboard.server</groupId>
+                <artifactId>extensions-api</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server</groupId>
+                <artifactId>extensions-core</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.extensions</groupId>
+                <artifactId>extension-rabbitmq</artifactId>
+                <classifier>extension</classifier>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.extensions</groupId>
+                <artifactId>extension-rest-api-call</artifactId>
+                <classifier>extension</classifier>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.extensions</groupId>
+                <artifactId>extension-kafka</artifactId>
+                <classifier>extension</classifier>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.common</groupId>
+                <artifactId>data</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.common</groupId>
+                <artifactId>message</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.common</groupId>
+                <artifactId>transport</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.transport</groupId>
+                <artifactId>http</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.transport</groupId>
+                <artifactId>coap</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server.transport</groupId>
+                <artifactId>mqtt</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server</groupId>
+                <artifactId>dao</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.thingsboard.server</groupId>
+                <artifactId>dao</artifactId>
+                <version>${project.version}</version>
+                <type>test-jar</type>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-security</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-web</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-websocket</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-autoconfigure</artifactId>
+                <version>${spring-boot.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-starter-test</artifactId>
+                <version>${spring-boot.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-context</artifactId>
+                <version>${spring.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-context-support</artifactId>
+                <version>${spring.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-tx</artifactId>
+                <version>${spring.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-web</artifactId>
+                <version>${spring.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework.security</groupId>
+                <artifactId>spring-security-test</artifactId>
+                <version>${spring-security.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>io.jsonwebtoken</groupId>
+                <artifactId>jjwt</artifactId>
+                <version>${jjwt.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>joda-time</groupId>
+                <artifactId>joda-time</artifactId>
+                <version>${joda-time.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.velocity</groupId>
+                <artifactId>velocity</artifactId>
+                <version>${velocity.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.velocity</groupId>
+                <artifactId>velocity-tools</artifactId>
+                <version>${velocity-tools.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>javax.servlet</groupId>
+                        <artifactId>servlet-api</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>com.rabbitmq</groupId>
+                <artifactId>amqp-client</artifactId>
+                <version>${rabbitmq.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>javax.mail</groupId>
+                <artifactId>mail</artifactId>
+                <version>${mail.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.curator</groupId>
+                <artifactId>curator-recipes</artifactId>
+                <version>${curator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.curator</groupId>
+                <artifactId>curator-test</artifactId>
+                <scope>test</scope>
+                <version>${curator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.jayway.jsonpath</groupId>
+                <artifactId>json-path</artifactId>
+                <version>${json-path.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>com.jayway.jsonpath</groupId>
+                <artifactId>json-path-assert</artifactId>
+                <version>${json-path.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-all</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.netty</groupId>
+                <artifactId>netty-handler</artifactId>
+                <version>${netty.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.datastax.cassandra</groupId>
+                <artifactId>cassandra-driver-core</artifactId>
+                <version>${cassandra.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.datastax.cassandra</groupId>
+                <artifactId>cassandra-driver-mapping</artifactId>
+                <version>${cassandra.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.datastax.cassandra</groupId>
+                <artifactId>cassandra-driver-extras</artifactId>
+                <version>${cassandra.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.commons</groupId>
+                <artifactId>commons-lang3</artifactId>
+                <version>${commons-lang3.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>commons-validator</groupId>
+                <artifactId>commons-validator</artifactId>
+                <version>${commons-validator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-databind</artifactId>
+                <version>${jackson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.github.fge</groupId>
+                <artifactId>json-schema-validator</artifactId>
+                <version>${json-schema-validator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.typesafe.akka</groupId>
+                <artifactId>akka-actor_${scala.version}</artifactId>
+                <version>${akka.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.typesafe.akka</groupId>
+                <artifactId>akka-slf4j_${scala.version}</artifactId>
+                <version>${akka.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.californium</groupId>
+                <artifactId>californium-core</artifactId>
+                <version>${californium.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.code.gson</groupId>
+                <artifactId>gson</artifactId>
+                <version>${gson.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>slf4j-api</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>log4j-over-slf4j</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.slf4j</groupId>
+                <artifactId>jul-to-slf4j</artifactId>
+                <version>${slf4j.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-core</artifactId>
+                <version>${logback.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>ch.qos.logback</groupId>
+                <artifactId>logback-classic</artifactId>
+                <version>${logback.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>${guava.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.protobuf</groupId>
+                <artifactId>protobuf-java</artifactId>
+                <version>${protobuf.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.grpc</groupId>
+                <artifactId>grpc-netty</artifactId>
+                <version>${grpc.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.grpc</groupId>
+                <artifactId>grpc-protobuf</artifactId>
+                <version>${grpc.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>io.grpc</groupId>
+                <artifactId>grpc-stub</artifactId>
+                <version>${grpc.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.springframework</groupId>
+                <artifactId>spring-test</artifactId>
+                <version>${spring.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>io.takari.junit</groupId>
+                <artifactId>takari-cpsuite</artifactId>
+                <version>${takari-cpsuite.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.cassandraunit</groupId>
+                <artifactId>cassandra-unit</artifactId>
+                <version>${cassandra-unit.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>junit</groupId>
+                <artifactId>junit</artifactId>
+                <version>${junit.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.mockito</groupId>
+                <artifactId>mockito-all</artifactId>
+                <version>${mockito.version}</version>
+                <scope>test</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.projectlombok</groupId>
+                <artifactId>lombok</artifactId>
+                <version>${lombok.version}</version>
+                <scope>provided</scope>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.kafka</groupId>
+                <artifactId>kafka_2.10</artifactId>
+                <version>${kafka.version}</version>
+                <exclusions>
+                    <exclusion>
+                        <groupId>org.slf4j</groupId>
+                        <artifactId>slf4j-log4j12</artifactId>
+                    </exclusion>
+                    <exclusion>
+                        <groupId>log4j</groupId>
+                        <artifactId>log4j</artifactId>
+                    </exclusion>
+                </exclusions>
+            </dependency>
+            <dependency>
+                <groupId>org.eclipse.paho</groupId>
+                <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+                <version>${paho.client.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.hazelcast</groupId>
+                <artifactId>hazelcast-spring</artifactId>
+                <version>${hazelcast.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>org.apache.curator</groupId>
+                <artifactId>curator-x-discovery</artifactId>
+                <version>${curator.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.hazelcast</groupId>
+                <artifactId>hazelcast-zookeeper</artifactId>
+                <version>${hazelcast-zookeeper.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.hazelcast</groupId>
+                <artifactId>hazelcast</artifactId>
+                <version>${hazelcast.version}</version>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
+    <repositories>
+        <repository>
+            <id>central</id>
+            <url>http://repo1.maven.org/maven2/</url>
+        </repository>
+        <repository>
+            <id>spring-snapshots</id>
+            <name>Spring Snapshots</name>
+            <url>https://repo.spring.io/snapshot</url>
+            <snapshots>
+                <enabled>true</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>spring-milestones</id>
+            <name>Spring Milestones</name>
+            <url>https://repo.spring.io/milestone</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+        </repository>
+        <repository>
+            <id>typesafe</id>
+            <name>Typesafe Repository</name>
+            <url>http://repo.typesafe.com/typesafe/releases/</url>
+        </repository>
+        <repository>
+            <id>sonatype</id>
+            <url>https://oss.sonatype.org/content/groups/public</url>
+        </repository>
+    </repositories>
+
+</project>

README.md 29(+29 -0)

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..dfd8735
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# iotrules
+IoT Rules Engine
+
+**Docker usage**
+
+**start platfrom using docker:**
+- install docker
+- cd to 'docker' folder
+- create folder for cassandra data directory on your local env (host)
+  - `mkdir /home/user/data_dir`
+- modify .env file to point to the directory created in previous step
+- start ./deploy.sh script to run all the services
+
+
+**start-up for local development** 
+
+cassandra with thingsboard schema (9042 and 9061 ports are exposed).  
+zookeper services (2181 port is exposed).  
+9042, 9061 and 2181 ports must be free so 'Thingsboard' server that is running outside docker container is able to connect to services.  
+you can change these ports in docker-compose.static.yml file to some others, but 'Thingsbaord' application.yml file must be updated accordingly.    
+if you would like to change cassandra port, change it to "9999:9042" for example and update cassandra.node_list entry in application.yml file to localhost:9999.  
+
+- install docker
+- cd to 'docker' folder
+- create folder for cassandra data directory on your local env (host)
+  - `mkdir /home/user/data_dir`
+- modify .env file to point to the directory created in previous step
+- start ./deploy_cassandra_zookeeper.sh script to run cassandra with thingsboard schema and zookeper services
+- Start boot class: _org.thingsboard.server.ThingsboardServerApplication_

tools/pom.xml 83(+83 -0)

diff --git a/tools/pom.xml b/tools/pom.xml
new file mode 100644
index 0000000..f022ac5
--- /dev/null
+++ b/tools/pom.xml
@@ -0,0 +1,83 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>server</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server</groupId>
+    <artifactId>tools</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard Server Tools</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>data</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.eclipse.paho</groupId>
+            <artifactId>org.eclipse.paho.client.mqttv3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+    </dependencies>
+</project>
diff --git a/tools/src/main/java/MqttStressTestClient.java b/tools/src/main/java/MqttStressTestClient.java
new file mode 100644
index 0000000..8202f94
--- /dev/null
+++ b/tools/src/main/java/MqttStressTestClient.java
@@ -0,0 +1,82 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.*;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttStressTestClient {
+
+    @Getter
+    private final String deviceToken;
+    @Getter
+    private final String clientId;
+    private final MqttClientPersistence persistence;
+    private final MqttAsyncClient client;
+    private final ResultAccumulator results;
+
+    public MqttStressTestClient(ResultAccumulator results, String brokerUri, String deviceToken) throws MqttException {
+        this.results = results;
+        this.clientId = MqttAsyncClient.generateClientId();
+        this.deviceToken = deviceToken;
+        this.persistence = new MemoryPersistence();
+        this.client = new MqttAsyncClient(brokerUri, clientId, persistence);
+    }
+
+    public void connect() throws MqttException {
+        MqttConnectOptions options = new MqttConnectOptions();
+        options.setUserName(deviceToken);
+        client.connect(options, null, new IMqttActionListener() {
+            @Override
+            public void onSuccess(IMqttToken iMqttToken) {
+                log.info("OnSuccess");
+            }
+
+            @Override
+            public void onFailure(IMqttToken iMqttToken, Throwable e) {
+                log.info("OnFailure", e);
+            }
+        });
+    }
+
+    public void disconnect() throws MqttException {
+        client.disconnect();
+    }
+
+    public void publishTelemetry(byte[] data) throws MqttException {
+        long sendTime = System.currentTimeMillis();
+        MqttMessage msg = new MqttMessage(data);
+        client.publish("v1/devices/me/telemetry", msg, null, new IMqttActionListener() {
+            @Override
+            public void onSuccess(IMqttToken asyncActionToken) {
+                long ackTime = System.currentTimeMillis();
+//                log.info("Delivery time: {}", ackTime - sendTime);
+                results.onResult(true, ackTime - sendTime);
+            }
+
+            @Override
+            public void onFailure(IMqttToken asyncActionToken, Throwable exception) {
+                long failTime = System.currentTimeMillis();
+//                log.info("Failure time: {}", failTime - sendTime);
+                results.onResult(false, failTime - sendTime);
+            }
+        });
+    }
+}
diff --git a/tools/src/main/java/MqttStressTestTool.java b/tools/src/main/java/MqttStressTestTool.java
new file mode 100644
index 0000000..4cb20f7
--- /dev/null
+++ b/tools/src/main/java/MqttStressTestTool.java
@@ -0,0 +1,88 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttStressTestTool {
+
+    private static final long TEST_DURATION = TimeUnit.MINUTES.toMillis(1);
+    private static final long TEST_ITERATION = TimeUnit.MILLISECONDS.toMillis(100);
+    private static final long TEST_SUB_ITERATION = TimeUnit.MILLISECONDS.toMillis(2);
+    private static final int DEVICE_COUNT = 100;
+    private static final String BASE_URL = "http://localhost:8080";
+    private static final String[] MQTT_URLS = {"tcp://localhost:1883"};
+//    private static final String[] MQTT_URLS = {"tcp://localhost:1883", "tcp://localhost:1884", "tcp://localhost:1885"};
+    private static final String USERNAME = "tenant@thingsboard.org";
+    private static final String PASSWORD = "tenant";
+
+
+    public static void main(String[] args) throws Exception {
+        ResultAccumulator results = new ResultAccumulator();
+
+        AtomicLong value = new AtomicLong(Long.MAX_VALUE);
+        log.info("value: {} ", value.incrementAndGet());
+
+        RestClient restClient = new RestClient(BASE_URL);
+        restClient.login(USERNAME, PASSWORD);
+
+        List<MqttStressTestClient> clients = new ArrayList<>();
+        for (int i = 0; i < DEVICE_COUNT; i++) {
+            Device device = restClient.createDevice("Device " + i);
+            DeviceCredentials credentials = restClient.getCredentials(device.getId());
+            String mqttURL = MQTT_URLS[i % MQTT_URLS.length];
+            MqttStressTestClient client = new MqttStressTestClient(results, mqttURL, credentials.getCredentialsId());
+            client.connect();
+            clients.add(client);
+        }
+        Thread.sleep(1000);
+
+
+        byte[] data = "{\"longKey\":73}".getBytes(StandardCharsets.UTF_8);
+        long startTime = System.currentTimeMillis();
+        int iterationsCount = (int) (TEST_DURATION / TEST_ITERATION);
+        int subIterationsCount = (int) (TEST_ITERATION / TEST_SUB_ITERATION);
+        if (clients.size() % subIterationsCount != 0) {
+            throw new IllegalArgumentException("Invalid parameter exception!");
+        }
+        for (int i = 0; i < iterationsCount; i++) {
+            for (int j = 0; j < subIterationsCount; j++) {
+                int packSize = clients.size() / subIterationsCount;
+                for (int k = 0; k < packSize; k++) {
+                    int clientIndex = packSize * j + k;
+                    clients.get(clientIndex).publishTelemetry(data);
+                }
+                Thread.sleep(TEST_SUB_ITERATION);
+            }
+        }
+        Thread.sleep(1000);
+        for (MqttStressTestClient client : clients) {
+            client.disconnect();
+        }
+        log.info("Results: {} took {}ms", results, System.currentTimeMillis() - startTime);
+    }
+
+}
diff --git a/tools/src/main/java/RestClient.java b/tools/src/main/java/RestClient.java
new file mode 100644
index 0000000..4a7a88b
--- /dev/null
+++ b/tools/src/main/java/RestClient.java
@@ -0,0 +1,72 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRequest;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.client.ClientHttpRequestExecution;
+import org.springframework.http.client.ClientHttpRequestInterceptor;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.client.support.HttpRequestWrapper;
+import org.springframework.web.client.RestTemplate;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RequiredArgsConstructor
+public class RestClient implements ClientHttpRequestInterceptor {
+    private static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
+    private final RestTemplate restTemplate = new RestTemplate();
+    private String token;
+    private final String baseURL;
+
+    public void login(String username, String password) {
+        Map<String, String> loginRequest = new HashMap<>();
+        loginRequest.put("username", username);
+        loginRequest.put("password", password);
+        ResponseEntity<JsonNode> tokenInfo = restTemplate.postForEntity(baseURL + "/api/auth/login", loginRequest, JsonNode.class);
+        this.token = tokenInfo.getBody().get("token").asText();
+        restTemplate.setInterceptors(Collections.singletonList(this));
+    }
+
+    public Device createDevice(String name) {
+        Device device = new Device();
+        device.setName(name);
+        return restTemplate.postForEntity(baseURL + "/api/device", device, Device.class).getBody();
+    }
+
+    public DeviceCredentials getCredentials(DeviceId id) {
+        return restTemplate.getForEntity(baseURL + "/api/device/" + id.getId().toString() + "/credentials", DeviceCredentials.class).getBody();
+    }
+
+    @Override
+    public ClientHttpResponse intercept(HttpRequest request, byte[] bytes, ClientHttpRequestExecution execution) throws IOException {
+        HttpRequest wrapper = new HttpRequestWrapper(request);
+        wrapper.getHeaders().set(JWT_TOKEN_HEADER_PARAM, "Bearer " + token);
+        return execution.execute(wrapper, bytes);
+    }
+
+}
diff --git a/tools/src/main/java/ResultAccumulator.java b/tools/src/main/java/ResultAccumulator.java
new file mode 100644
index 0000000..e7352ea
--- /dev/null
+++ b/tools/src/main/java/ResultAccumulator.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class ResultAccumulator {
+
+    private AtomicLong minTime = new AtomicLong(Long.MAX_VALUE);
+    private AtomicLong maxTime = new AtomicLong(Long.MIN_VALUE);
+    private AtomicLong timeSpentCount = new AtomicLong();
+    private AtomicInteger successCount = new AtomicInteger();
+    private AtomicInteger errorCount = new AtomicInteger();
+
+    public void onResult(boolean success, long timeSpent) {
+        if (success) {
+            successCount.incrementAndGet();
+        } else {
+            errorCount.incrementAndGet();
+        }
+        timeSpentCount.addAndGet(timeSpent);
+
+        while (!setMax(timeSpent)) ;
+        while (!setMin(timeSpent)) ;
+    }
+
+    private boolean setMax(long timeSpent) {
+        long curMax = maxTime.get();
+        long newMax = Math.max(curMax, timeSpent);
+        return maxTime.compareAndSet(curMax, newMax);
+    }
+
+    private boolean setMin(long timeSpent) {
+        long curMin = minTime.get();
+        long newMin = Math.min(curMin, timeSpent);
+        return minTime.compareAndSet(curMin, newMin);
+    }
+
+
+    public int getSuccessCount() {
+        return successCount.get();
+    }
+
+    public int getErrorCount() {
+        return errorCount.get();
+    }
+
+    public long getTimeSpent() {
+        return timeSpentCount.get();
+    }
+
+    public double getAvgTimeSpent() {
+        return ((double) getTimeSpent()) / (getSuccessCount() + getErrorCount());
+    }
+
+    @Override
+    public String toString() {
+        return "ResultAccumulator{" +
+                "successCount=" + getSuccessCount() +
+                ", errorCount=" + getErrorCount() +
+                ", totalTime=" + getTimeSpent() +
+                ", avgTime=" + getAvgTimeSpent() +
+                ", minTime=" + minTime.get() +
+                ", maxTime=" + maxTime.get() +
+                '}';
+    }
+}
diff --git a/tools/src/main/shell/keygen.properties b/tools/src/main/shell/keygen.properties
new file mode 100644
index 0000000..6016bda
--- /dev/null
+++ b/tools/src/main/shell/keygen.properties
@@ -0,0 +1,8 @@
+HOSTNAME="$(hostname)"
+PASSWORD="password"
+
+CLIENT_TRUSTSTORE="client_truststore.crt"
+
+SERVER_KEY_ALIAS="serveralias"
+SERVER_FILE_PREFIX="mqttserver"
+SERVER_KEYSTORE_DIR="../../../../application/src/main/resources/keystore/"
\ No newline at end of file
diff --git a/tools/src/main/shell/keygen.sh b/tools/src/main/shell/keygen.sh
new file mode 100755
index 0000000..25b3157
--- /dev/null
+++ b/tools/src/main/shell/keygen.sh
@@ -0,0 +1,57 @@
+#!/bin/sh
+#
+# Copyright © 2016 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+
+. keygen.properties
+
+echo "Generating SSL Key Pair..."
+
+keytool -genkeypair -v \
+  -alias $SERVER_KEY_ALIAS \
+  -dname "CN=$HOSTNAME, OU=Thingsboard, O=Thingsboard, L=Piscataway, ST=NJ, C=US" \
+  -keystore $SERVER_FILE_PREFIX.jks \
+  -keypass $PASSWORD \
+  -storepass $PASSWORD \
+  -keyalg RSA \
+  -keysize 2048 \
+  -validity 9999
+
+keytool -export \
+  -alias $SERVER_KEY_ALIAS \
+  -keystore $SERVER_FILE_PREFIX.jks \
+  -file $CLIENT_TRUSTSTORE -rfc \
+  -storepass $PASSWORD
+
+read -p  "Do you want to copy $SERVER_FILE_PREFIX.jks to server directory? " yn
+    case $yn in
+        [Yy]) echo "Please, specify destination dir: "
+             read -p "(Default: $SERVER_KEYSTORE_DIR): " dir
+             if [[ !  -z  $dir  ]]; then
+                DESTINATION=$dir;
+             else
+                DESTINATION=$SERVER_KEYSTORE_DIR
+             fi;
+             cp $SERVER_FILE_PREFIX.jks $DESTINATION
+             if [ $? -ne 0 ]; then
+                echo "Failed to copy keystore file."
+             else
+                echo "File copied successfully."
+             fi
+             break;;
+        * ) ;;
+    esac
+echo "Done."
diff --git a/tools/src/main/shell/securemqttclient.py b/tools/src/main/shell/securemqttclient.py
new file mode 100644
index 0000000..a3fbf17
--- /dev/null
+++ b/tools/src/main/shell/securemqttclient.py
@@ -0,0 +1,55 @@
+#
+# Copyright © 2016 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import paho.mqtt.client as mqtt
+import ssl, socket
+
+# The callback for when the client receives a CONNACK response from the server.
+def on_connect(client, userdata, rc):
+   print('Connected with result code '+str(rc))
+   # Subscribing in on_connect() means that if we lose the connection and
+   # reconnect then subscriptions will be renewed.
+   client.subscribe('v1/devices/me/attributes')
+   client.subscribe('v1/devices/me/attributes/response/+')
+   client.subscribe('v1/devices/me/rpc/request/+')
+
+
+# The callback for when a PUBLISH message is received from the server.
+def on_message(client, userdata, msg):
+   print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload)
+   if msg.topic.startswith( 'v1/devices/me/rpc/request/'):
+       requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)]
+       print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!'
+       client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1)
+
+
+client = mqtt.Client()
+client.on_connect = on_connect
+client.on_message = on_message
+client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1)
+
+client.tls_set(ca_certs="client_truststore.crt", certfile=None, keyfile=None, cert_reqs=ssl.CERT_REQUIRED,
+               tls_version=ssl.PROTOCOL_TLSv1, ciphers=None);
+client.username_pw_set("TEST_TOKEN")
+client.tls_insecure_set(False)
+client.connect(socket.gethostname(), 1883, 1)
+
+
+# Blocking call that processes network traffic, dispatches callbacks and
+# handles reconnecting.
+# Other loop*() functions are available that give a threaded interface and a
+# manual interface.
+client.loop_forever()
diff --git a/tools/src/main/shell/simplemqttclient.py b/tools/src/main/shell/simplemqttclient.py
new file mode 100644
index 0000000..166d374
--- /dev/null
+++ b/tools/src/main/shell/simplemqttclient.py
@@ -0,0 +1,50 @@
+#
+# Copyright © 2016 The Thingsboard Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import paho.mqtt.client as mqtt
+
+# The callback for when the client receives a CONNACK response from the server.
+def on_connect(client, userdata, rc):
+   print('Connected with result code '+str(rc))
+   # Subscribing in on_connect() means that if we lose the connection and
+   # reconnect then subscriptions will be renewed.
+   client.subscribe('v1/devices/me/attributes')
+   client.subscribe('v1/devices/me/attributes/response/+')
+   client.subscribe('v1/devices/me/rpc/request/+')
+
+
+# The callback for when a PUBLISH message is received from the server.
+def on_message(client, userdata, msg):
+   print 'Topic: ' + msg.topic + '\nMessage: ' + str(msg.payload)
+   if msg.topic.startswith( 'v1/devices/me/rpc/request/'):
+       requestId = msg.topic[len('v1/devices/me/rpc/request/'):len(msg.topic)]
+       print 'This is a RPC call. RequestID: ' + requestId + '. Going to reply now!'
+       client.publish('v1/devices/me/rpc/response/' + requestId, "{\"value1\":\"A\", \"value2\":\"B\"}", 1)
+
+
+client = mqtt.Client()
+client.on_connect = on_connect
+client.on_message = on_message
+client.publish('v1/devices/me/attributes/request/1', "{\"clientKeys\":\"model\"}", 1)
+
+client.username_pw_set("TEST_TOKEN")
+client.connect('127.0.0.1', 1883, 1)
+
+# Blocking call that processes network traffic, dispatches callbacks and
+# handles reconnecting.
+# Other loop*() functions are available that give a threaded interface and a
+# manual interface.
+client.loop_forever()
diff --git a/transport/coap/pom.xml b/transport/coap/pom.xml
new file mode 100644
index 0000000..6ddf64b
--- /dev/null
+++ b/transport/coap/pom.xml
@@ -0,0 +1,84 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard.server</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>transport</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server.transport</groupId>
+    <artifactId>coap</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard COAP Transport</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>transport</artifactId>
+        </dependency>    
+        <dependency>
+            <groupId>org.eclipse.californium</groupId>
+            <artifactId>californium-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java
new file mode 100644
index 0000000..0d64988
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/CoapTransportAdaptor.java
@@ -0,0 +1,25 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.adaptors;
+
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.thingsboard.server.common.transport.TransportAdaptor;
+import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
+
+public interface CoapTransportAdaptor extends TransportAdaptor<CoapSessionCtx, Request, Response> {
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
new file mode 100644
index 0000000..26a9056
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/adaptors/JsonCoapAdaptor.java
@@ -0,0 +1,265 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.adaptors;
+
+import java.util.*;
+
+import com.google.gson.JsonElement;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionContext;
+import org.thingsboard.server.common.msg.session.ToDeviceMsg;
+import org.thingsboard.server.common.msg.session.ex.ProcessingTimeoutException;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.springframework.stereotype.Component;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import org.thingsboard.server.transport.coap.CoapTransportResource;
+import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
+
+@Component("JsonCoapAdaptor")
+@Slf4j
+public class JsonCoapAdaptor implements CoapTransportAdaptor {
+
+    @Override
+    public AdaptorToSessionActorMsg convertToActorMsg(CoapSessionCtx ctx, MsgType type, Request inbound) throws AdaptorException {
+        FromDeviceMsg msg = null;
+        switch (type) {
+            case POST_TELEMETRY_REQUEST:
+                msg = convertToTelemetryUploadRequest(ctx, inbound);
+                break;
+            case POST_ATTRIBUTES_REQUEST:
+                msg = convertToUpdateAttributesRequest(ctx, inbound);
+                break;
+            case GET_ATTRIBUTES_REQUEST:
+                msg = convertToGetAttributesRequest(ctx, inbound);
+                break;
+            case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+                msg = new RpcSubscribeMsg();
+                break;
+            case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+                msg = new RpcUnsubscribeMsg();
+                break;
+            case TO_DEVICE_RPC_RESPONSE:
+                msg = convertToDeviceRpcResponse(ctx, inbound);
+                break;
+            case TO_SERVER_RPC_REQUEST:
+                msg = convertToServerRpcRequest(ctx, inbound);
+                break;
+            case SUBSCRIBE_ATTRIBUTES_REQUEST:
+                msg = new AttributesSubscribeMsg();
+                break;
+            case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+                msg = new AttributesUnsubscribeMsg();
+                break;
+            default:
+                log.warn("[{}] Unsupported msg type: {}!", ctx.getSessionId(), type);
+                throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + type + "!"));
+        }
+        return new BasicAdaptorToSessionActorMsg(ctx, msg);
+    }
+
+    private FromDeviceMsg convertToDeviceRpcResponse(CoapSessionCtx ctx, Request inbound) throws AdaptorException {
+        Optional<Integer> requestId = CoapTransportResource.getRequestId(inbound);
+        String payload = validatePayload(ctx, inbound);
+        JsonObject response = new JsonParser().parse(payload).getAsJsonObject();
+        return new ToDeviceRpcResponseMsg(
+                requestId.orElseThrow(() -> new AdaptorException("Request id is missing!")),
+                response.get("response").toString());
+    }
+
+    private FromDeviceMsg convertToServerRpcRequest(CoapSessionCtx ctx, Request inbound) throws AdaptorException {
+
+        String payload = validatePayload(ctx, inbound);
+
+        return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), 0);
+    }
+
+    @Override
+    public Optional<Response> convertToAdaptorMsg(CoapSessionCtx ctx, SessionActorToAdaptorMsg source) throws AdaptorException {
+        ToDeviceMsg msg = source.getMsg();
+        switch (msg.getMsgType()) {
+            case STATUS_CODE_RESPONSE:
+            case TO_DEVICE_RPC_RESPONSE_ACK:
+                return Optional.of(convertStatusCodeResponse((StatusCodeResponse) msg));
+            case GET_ATTRIBUTES_RESPONSE:
+                return Optional.of(convertGetAttributesResponse((GetAttributesResponse) msg));
+            case ATTRIBUTES_UPDATE_NOTIFICATION:
+                return Optional.of(convertNotificationResponse(ctx, (AttributesUpdateNotification) msg));
+            case TO_DEVICE_RPC_REQUEST:
+                return Optional.of(convertToDeviceRpcRequest(ctx, (ToDeviceRpcRequestMsg) msg));
+            case TO_SERVER_RPC_RESPONSE:
+                return Optional.of(convertToServerRpcResponse(ctx, (ToServerRpcResponseMsg) msg));
+            case RULE_ENGINE_ERROR:
+                return Optional.of(convertToRuleEngineErrorResponse(ctx, (RuleEngineErrorMsg) msg));
+            default:
+                log.warn("[{}] Unsupported msg type: {}!", source.getSessionId(), msg.getMsgType());
+                throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + msg.getMsgType() + "!"));
+        }
+    }
+
+    private Response convertToRuleEngineErrorResponse(CoapSessionCtx ctx, RuleEngineErrorMsg msg) {
+        ResponseCode status = ResponseCode.INTERNAL_SERVER_ERROR;
+        switch (msg.getError()) {
+            case PLUGIN_TIMEOUT:
+                status = ResponseCode.GATEWAY_TIMEOUT;
+                break;
+            default:
+                if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+                    status = ResponseCode.BAD_REQUEST;
+                }
+                break;
+        }
+        Response response = new Response(status);
+        response.setPayload(JsonConverter.toErrorJson(msg.getErrorMsg()).toString());
+        return response;
+    }
+
+    private Response convertNotificationResponse(CoapSessionCtx ctx, AttributesUpdateNotification msg) {
+        return getObserveNotification(ctx, JsonConverter.toJson(msg.getData(), false));
+    }
+
+    private Response convertToDeviceRpcRequest(CoapSessionCtx ctx, ToDeviceRpcRequestMsg msg) {
+        return getObserveNotification(ctx, JsonConverter.toJson(msg, true));
+    }
+
+    private Response getObserveNotification(CoapSessionCtx ctx, JsonObject json) {
+        Response response = new Response(ResponseCode.CONTENT);
+        response.getOptions().setObserve(ctx.nextSeqNumber());
+        response.setPayload(json.toString());
+        return response;
+    }
+
+    private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+        String payload = validatePayload(ctx, inbound);
+        try {
+            return JsonConverter.convertToAttributes(new JsonParser().parse(payload));
+        } catch (IllegalStateException | JsonSyntaxException ex) {
+            throw new AdaptorException(ex);
+        }
+    }
+
+    private FromDeviceMsg convertToGetAttributesRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+        List<String> queryElements = inbound.getOptions().getUriQuery();
+        if (queryElements == null || queryElements.size() == 0) {
+            log.warn("[{}] Query is empty!", ctx.getSessionId());
+            throw new AdaptorException(new IllegalArgumentException("Query is empty!"));
+        }
+
+        Set<String> clientKeys = toKeys(ctx, queryElements, "clientKeys");
+        Set<String> sharedKeys = toKeys(ctx, queryElements, "sharedKeys");
+        if (clientKeys.isEmpty() && sharedKeys.isEmpty()) {
+            throw new AdaptorException("No clientKeys and serverKeys parameters!");
+        }
+        return new BasicGetAttributesRequest(0, clientKeys, sharedKeys);
+    }
+
+    private Set<String> toKeys(SessionContext ctx, List<String> queryElements, String attributeName) throws AdaptorException {
+        String keys = null;
+        for (String queryElement : queryElements) {
+            String[] queryItem = queryElement.split("=");
+            if (queryItem.length == 2 && queryItem[0].equals(attributeName)) {
+                keys = queryItem[1];
+            }
+        }
+        if (!StringUtils.isEmpty(keys)) {
+            return new HashSet<>(Arrays.asList(keys.split(",")));
+        } else {
+            return Collections.emptySet();
+        }
+    }
+
+    private TelemetryUploadRequest convertToTelemetryUploadRequest(SessionContext ctx, Request inbound) throws AdaptorException {
+        String payload = validatePayload(ctx, inbound);
+        try {
+            return JsonConverter.convertToTelemetry(new JsonParser().parse(payload));
+        } catch (IllegalStateException | JsonSyntaxException ex) {
+            throw new AdaptorException(ex);
+        }
+    }
+
+    private Response convertStatusCodeResponse(StatusCodeResponse msg) {
+        if (msg.isSuccess()) {
+            Integer code = msg.getData().get();
+            if (code == 200) {
+                return new Response(ResponseCode.VALID);
+            } else {
+                return new Response(ResponseCode.CREATED);
+            }
+        } else {
+            return convertError(msg.getError().get());
+        }
+    }
+
+    private String validatePayload(SessionContext ctx, Request inbound) throws AdaptorException {
+        String payload = inbound.getPayloadString();
+        if (payload == null) {
+            log.warn("[{}] Payload is empty!", ctx.getSessionId());
+            throw new AdaptorException(new IllegalArgumentException("Payload is empty!"));
+        }
+        return payload;
+    }
+
+    private Response convertToServerRpcResponse(SessionContext ctx, ToServerRpcResponseMsg msg) {
+        if (msg.isSuccess()) {
+            Response response = new Response(ResponseCode.CONTENT);
+            JsonElement result = JsonConverter.toJson(msg);
+            response.setPayload(result.toString());
+            return response;
+        } else {
+            return convertError(new RuntimeException("Server RPC response is empty!"));
+        }
+    }
+
+    private Response convertGetAttributesResponse(GetAttributesResponse msg) {
+        if (msg.isSuccess()) {
+            AttributesKVMsg payload = msg.getData().get();
+            if (payload.getClientAttributes().isEmpty() && payload.getSharedAttributes().isEmpty()) {
+                return new Response(ResponseCode.NOT_FOUND);
+            } else {
+                Response response = new Response(ResponseCode.CONTENT);
+                JsonObject result = JsonConverter.toJson(payload, false);
+                response.setPayload(result.toString());
+                return response;
+            }
+        } else {
+            return convertError(msg.getError().get());
+        }
+    }
+
+    private Response convertError(Exception exception) {
+        log.warn("Converting exception: {}", exception.getMessage(), exception);
+        if (exception instanceof ProcessingTimeoutException) {
+            return new Response(ResponseCode.SERVICE_UNAVAILABLE);
+        } else {
+            return new Response(ResponseCode.INTERNAL_SERVER_ERROR);
+        }
+    }
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java
new file mode 100644
index 0000000..bff0670
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/client/DeviceEmulator.java
@@ -0,0 +1,179 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.client;
+
+import java.io.IOException;
+import java.util.Random;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.CoapHandler;
+import org.eclipse.californium.core.CoapResponse;
+import org.eclipse.californium.core.coap.MediaTypeRegistry;
+import org.thingsboard.server.common.msg.session.FeatureType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+@Slf4j
+public class DeviceEmulator {
+
+    public static final String SN = "SN-" + new Random().nextInt(1000);
+    public static final String MODEL = "Model " + new Random().nextInt(1000);
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private final String host;
+    private final int port;
+    private final String token;
+
+    private CoapClient attributesClient;
+    private CoapClient telemetryClient;
+    private CoapClient rpcClient;
+    private String[] keys;
+    private ExecutorService executor = Executors.newFixedThreadPool(1);
+    private AtomicInteger seq = new AtomicInteger(100);
+
+    private DeviceEmulator(String host, int port, String token, String keys) {
+        this.host = host;
+        this.port = port;
+        this.token = token;
+        this.attributesClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.ATTRIBUTES));
+        this.telemetryClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.TELEMETRY));
+        this.rpcClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.RPC));
+        this.keys = keys.split(",");
+    }
+
+    public void start() {
+        executor.submit(new Runnable() {
+
+            @Override
+            public void run() {
+                try {
+                    sendObserveRequest(rpcClient);
+                    while (!Thread.interrupted()) {
+
+
+                        sendRequest(attributesClient, createAttributesRequest());
+                        sendRequest(telemetryClient, createTelemetryRequest());
+
+                        Thread.sleep(1000);
+                    }
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+
+            private void sendRequest(CoapClient client, JsonNode request) throws JsonProcessingException {
+                CoapResponse telemetryResponse = client.setTimeout(60000).post(mapper.writeValueAsString(request),
+                        MediaTypeRegistry.APPLICATION_JSON);
+                log.info("Response: {}, {}", telemetryResponse.getCode(), telemetryResponse.getResponseText());
+            }
+
+            private void sendObserveRequest(CoapClient client) throws JsonProcessingException {
+                client.observe(new CoapHandler() {
+                    @Override
+                    public void onLoad(CoapResponse coapResponse) {
+                        log.info("Command: {}, {}", coapResponse.getCode(), coapResponse.getResponseText());
+                        try {
+                            JsonNode node = mapper.readTree(coapResponse.getResponseText());
+                            int requestId = node.get("id").asInt();
+                            String method = node.get("method").asText();
+                            ObjectNode params = (ObjectNode) node.get("params");
+                            ObjectNode response = mapper.createObjectNode();
+                            response.put("id", requestId);
+                            response.set("response", params);
+                            log.info("Command Response: {}, {}", requestId, mapper.writeValueAsString(response));
+                            CoapClient commandResponseClient = new CoapClient(getFeatureTokenUrl(host, port, token, FeatureType.RPC));
+                            commandResponseClient.post(new CoapHandler() {
+                                @Override
+                                public void onLoad(CoapResponse response) {
+                                    log.info("Command Response Ack: {}, {}", response.getCode(), response.getResponseText());
+                                }
+
+                                @Override
+                                public void onError() {
+
+                                }
+                            }, mapper.writeValueAsString(response), MediaTypeRegistry.APPLICATION_JSON);
+
+                        } catch (IOException e) {
+                            e.printStackTrace();
+                        }
+                    }
+
+                    @Override
+                    public void onError() {
+
+                    }
+                });
+            }
+
+        });
+    }
+
+    private ObjectNode createAttributesRequest() {
+        ObjectNode element = mapper.createObjectNode();
+        element.put("serialNumber", SN);
+        element.put("model", MODEL);
+        return element;
+    }
+
+    private ArrayNode createTelemetryRequest() {
+        ArrayNode rootNode = mapper.createArrayNode();
+        for (String key : keys) {
+            ObjectNode element = mapper.createObjectNode();
+            element.put(key, seq.incrementAndGet());
+            rootNode.add(element);
+        }
+        return rootNode;
+    }
+
+    protected void stop() {
+        executor.shutdownNow();
+    }
+
+    public static void main(String args[]) {
+        if (args.length != 4) {
+            System.out.println("Usage: java -jar " + DeviceEmulator.class.getSimpleName() + ".jar host port device_token keys");
+        }
+        final DeviceEmulator emulator = new DeviceEmulator(args[0], Integer.parseInt(args[1]), args[2], args[3]);
+        emulator.start();
+        Runtime.getRuntime().addShutdownHook(new Thread() {
+            @Override
+            public void run() {
+                emulator.stop();
+            }
+        });
+    }
+
+
+    private String getFeatureTokenUrl(String host, int port, String token, FeatureType featureType) {
+        return getBaseUrl(host, port) + token + "/" + featureType.name().toLowerCase();
+    }
+
+    private String getBaseUrl(String host, int port) {
+        return "coap://" + host + ":" + port + "/api/v1/";
+    }
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
new file mode 100644
index 0000000..32ea0f5
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportResource.java
@@ -0,0 +1,222 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import java.lang.reflect.Field;
+import java.util.List;
+import java.util.Optional;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.network.Exchange;
+import org.eclipse.californium.core.network.ExchangeObserver;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+import org.eclipse.californium.core.server.resources.Resource;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
+import org.thingsboard.server.transport.coap.session.CoapExchangeObserverProxy;
+import org.thingsboard.server.transport.coap.session.CoapSessionCtx;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.ReflectionUtils;
+
+@Slf4j
+public class CoapTransportResource extends CoapResource {
+    // coap://localhost:port/api/v1/DEVICE_TOKEN/[attributes|telemetry|rpc[/requestId]]
+    private static final int ACCESS_TOKEN_POSITION = 3;
+    private static final int FEATURE_TYPE_POSITION = 4;
+    private static final int REQUEST_ID_POSITION = 5;
+
+    private final CoapTransportAdaptor adaptor;
+    private final SessionMsgProcessor processor;
+    private final DeviceAuthService authService;
+    private final Field observerField;
+    private final long timeout;
+
+    public CoapTransportResource(SessionMsgProcessor processor, DeviceAuthService authService, CoapTransportAdaptor adaptor, String name, long timeout) {
+        super(name);
+        this.processor = processor;
+        this.authService = authService;
+        this.adaptor = adaptor;
+        this.timeout = timeout;
+        // This is important to turn off existing observable logic in
+        // CoapResource. We will have our own observe monitoring due to 1:1
+        // observe relationship.
+        this.setObservable(false);
+        observerField = ReflectionUtils.findField(Exchange.class, "observer");
+        observerField.setAccessible(true);
+    }
+
+    @Override
+    public void handleGET(CoapExchange exchange) {
+        Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
+        if (!featureType.isPresent()) {
+            log.trace("Missing feature type parameter");
+            exchange.respond(ResponseCode.BAD_REQUEST);
+        } else if (featureType.get() == FeatureType.TELEMETRY) {
+            log.trace("Can't fetch/subscribe to timeseries updates");
+            exchange.respond(ResponseCode.BAD_REQUEST);
+        } else if (exchange.getRequestOptions().hasObserve()) {
+            boolean unsubscribe = exchange.getRequestOptions().getObserve() == 1;
+            MsgType msgType;
+            if (featureType.get() == FeatureType.RPC) {
+                msgType = unsubscribe ? MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST : MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST;
+            } else {
+                msgType = unsubscribe ? MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST : MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST;
+            }
+            Optional<SessionId> sessionId = processRequest(exchange, msgType);
+            if (sessionId.isPresent()) {
+                if (exchange.getRequestOptions().getObserve() == 1) {
+                    exchange.respond(ResponseCode.VALID);
+                }
+            }
+        } else if (featureType.get() == FeatureType.ATTRIBUTES) {
+            processRequest(exchange, MsgType.GET_ATTRIBUTES_REQUEST);
+        } else {
+            log.trace("Invalid feature type parameter");
+            exchange.respond(ResponseCode.BAD_REQUEST);
+        }
+    }
+
+    @Override
+    public void handlePOST(CoapExchange exchange) {
+        Optional<FeatureType> featureType = getFeatureType(exchange.advanced().getRequest());
+        if (!featureType.isPresent()) {
+            log.trace("Missing feature type parameter");
+            exchange.respond(ResponseCode.BAD_REQUEST);
+        } else {
+            switch (featureType.get()) {
+                case ATTRIBUTES:
+                    processRequest(exchange, MsgType.POST_ATTRIBUTES_REQUEST);
+                    break;
+                case TELEMETRY:
+                    processRequest(exchange, MsgType.POST_TELEMETRY_REQUEST);
+                    break;
+                case RPC:
+                    Optional<Integer> requestId = getRequestId(exchange.advanced().getRequest());
+                    if (requestId.isPresent()) {
+                        processRequest(exchange, MsgType.TO_DEVICE_RPC_RESPONSE);
+                    } else {
+                        processRequest(exchange, MsgType.TO_SERVER_RPC_REQUEST);
+                    }
+                    break;
+            }
+        }
+    }
+
+    private Optional<SessionId> processRequest(CoapExchange exchange, MsgType type) {
+        log.trace("Processing {}", exchange.advanced().getRequest());
+        exchange.accept();
+        Exchange advanced = exchange.advanced();
+        Request request = advanced.getRequest();
+
+        Optional<DeviceCredentialsFilter> credentials = decodeCredentials(request);
+        if (!credentials.isPresent()) {
+            exchange.respond(ResponseCode.BAD_REQUEST);
+            return Optional.empty();
+        }
+
+        CoapSessionCtx ctx = new CoapSessionCtx(exchange, adaptor, processor, authService, timeout);
+
+        if (!ctx.login(credentials.get())) {
+            exchange.respond(ResponseCode.UNAUTHORIZED);
+            return Optional.empty();
+        }
+
+        AdaptorToSessionActorMsg msg;
+        try {
+            switch (type) {
+                case GET_ATTRIBUTES_REQUEST:
+                case POST_ATTRIBUTES_REQUEST:
+                case POST_TELEMETRY_REQUEST:
+                case TO_DEVICE_RPC_RESPONSE:
+                case TO_SERVER_RPC_REQUEST:
+                    ctx.setSessionType(SessionType.SYNC);
+                    msg = adaptor.convertToActorMsg(ctx, type, request);
+                    break;
+                case SUBSCRIBE_ATTRIBUTES_REQUEST:
+                case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+                    ExchangeObserver systemObserver = (ExchangeObserver) observerField.get(advanced);
+                    advanced.setObserver(new CoapExchangeObserverProxy(systemObserver, ctx));
+                case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+                case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+                    ctx.setSessionType(SessionType.ASYNC);
+                    msg = adaptor.convertToActorMsg(ctx, type, request);
+                    break;
+                default:
+                    log.trace("[{}] Unsupported msg type: {}", ctx.getSessionId(), type);
+                    throw new IllegalArgumentException("Unsupported msg type: " + type);
+            }
+            log.trace("Processing msg: {}", msg);
+            processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+        } catch (AdaptorException e) {
+            log.debug("Failed to decode payload {}", e);
+            exchange.respond(ResponseCode.BAD_REQUEST, e.getMessage());
+            return Optional.empty();
+        } catch (IllegalArgumentException | IllegalAccessException e) {
+            log.debug("Failed to process payload {}", e);
+            exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR, e.getMessage());
+        }
+        return Optional.of(ctx.getSessionId());
+    }
+
+    private Optional<DeviceCredentialsFilter> decodeCredentials(Request request) {
+        List<String> uriPath = request.getOptions().getUriPath();
+        DeviceCredentialsFilter credentials = null;
+        if (uriPath.size() >= ACCESS_TOKEN_POSITION) {
+            credentials = new DeviceTokenCredentials(uriPath.get(ACCESS_TOKEN_POSITION - 1));
+        }
+        return Optional.ofNullable(credentials);
+    }
+
+    private Optional<FeatureType> getFeatureType(Request request) {
+        List<String> uriPath = request.getOptions().getUriPath();
+        try {
+            if (uriPath.size() >= FEATURE_TYPE_POSITION) {
+                return Optional.of(FeatureType.valueOf(uriPath.get(FEATURE_TYPE_POSITION - 1).toUpperCase()));
+            }
+        } catch (RuntimeException e) {
+            log.warn("Failed to decode feature type: {}", uriPath);
+        }
+        return Optional.empty();
+    }
+
+    public static Optional<Integer> getRequestId(Request request) {
+        List<String> uriPath = request.getOptions().getUriPath();
+        try {
+            if (uriPath.size() >= REQUEST_ID_POSITION) {
+                return Optional.of(Integer.valueOf(uriPath.get(REQUEST_ID_POSITION - 1)));
+            }
+        } catch (RuntimeException e) {
+            log.warn("Failed to decode feature type: {}", uriPath);
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public Resource getChild(String name) {
+        return this;
+    }
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
new file mode 100644
index 0000000..31ead0c
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/CoapTransportService.java
@@ -0,0 +1,96 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapResource;
+import org.eclipse.californium.core.CoapServer;
+import org.eclipse.californium.core.network.CoapEndpoint;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+
+@Service("CoapTransportService")
+@Slf4j
+public class CoapTransportService {
+
+    private static final String V1 = "v1";
+    private static final String API = "api";
+
+    private CoapServer server;
+
+    @Autowired(required = false)
+    private ApplicationContext appContext;
+
+    @Autowired(required = false)
+    private SessionMsgProcessor processor;
+
+    @Autowired(required = false)
+    private DeviceAuthService authService;
+
+
+    @Value("${coap.bind_address}")
+    private String host;
+    @Value("${coap.bind_port}")
+    private Integer port;
+    @Value("${coap.adaptor}")
+    private String adaptorName;
+    @Value("${coap.timeout}")
+    private Long timeout;
+
+    private CoapTransportAdaptor adaptor;
+
+    @PostConstruct
+    public void init() throws UnknownHostException {
+        log.info("Starting CoAP transport...");
+        log.info("Lookup CoAP transport adaptor {}", adaptorName);
+        this.adaptor = (CoapTransportAdaptor) appContext.getBean(adaptorName);
+        log.info("Starting CoAP transport server");
+        this.server = new CoapServer();
+        createResources();
+        InetAddress addr = InetAddress.getByName(host);
+        InetSocketAddress sockAddr = new InetSocketAddress(addr, port);
+        server.addEndpoint(new CoapEndpoint(sockAddr));
+        server.start();
+        log.info("CoAP transport started!");
+    }
+
+    private void createResources() {
+        CoapResource api = new CoapResource(API);
+        api.add(new CoapTransportResource(processor, authService, adaptor, V1, timeout));
+        server.add(api);
+    }
+
+    @PreDestroy
+    public void shutdown() {
+        log.info("Stopping CoAP transport!");
+        this.server.destroy();
+        log.info("CoAP transport stopped!");
+    }
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java
new file mode 100644
index 0000000..e883955
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapExchangeObserverProxy.java
@@ -0,0 +1,38 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.session;
+
+import org.eclipse.californium.core.network.Exchange;
+import org.eclipse.californium.core.network.ExchangeObserver;
+
+public class CoapExchangeObserverProxy implements ExchangeObserver {
+
+    private final ExchangeObserver proxy;
+    private final CoapSessionCtx ctx;
+
+    public CoapExchangeObserverProxy(ExchangeObserver proxy, CoapSessionCtx ctx) {
+        super();
+        this.proxy = proxy;
+        this.ctx = ctx;
+    }
+
+    @Override
+    public void completed(Exchange exchange) {
+        proxy.completed(exchange);
+        ctx.close();
+    }
+
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
new file mode 100644
index 0000000..a61f0bf
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionCtx.java
@@ -0,0 +1,143 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.session;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.Request;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.server.resources.CoapExchange;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+import org.thingsboard.server.common.msg.session.ex.SessionAuthException;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+import org.thingsboard.server.transport.coap.adaptors.CoapTransportAdaptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.atomic.AtomicInteger;
+@Slf4j
+public class CoapSessionCtx extends DeviceAwareSessionContext {
+
+    private final SessionId sessionId;
+    private final CoapExchange exchange;
+    private final CoapTransportAdaptor adaptor;
+    private final String token;
+    private final long timeout;
+    private SessionType sessionType;
+    private final AtomicInteger seqNumber = new AtomicInteger(2);
+
+    public CoapSessionCtx(CoapExchange exchange, CoapTransportAdaptor adaptor, SessionMsgProcessor processor, DeviceAuthService authService, long timeout) {
+        super(processor, authService);
+        Request request = exchange.advanced().getRequest();
+        this.token = request.getTokenString();
+        this.sessionId = new CoapSessionId(request.getSource().getHostAddress(), request.getSourcePort(), this.token);
+        this.exchange = exchange;
+        this.adaptor = adaptor;
+        this.timeout = timeout;
+    }
+
+
+    @Override
+    public void onMsg(SessionActorToAdaptorMsg msg) throws SessionException {
+        try {
+            adaptor.convertToAdaptorMsg(this, msg).ifPresent(this::pushToNetwork);
+        } catch (AdaptorException e) {
+            logAndWrap(e);
+        }
+    }
+
+    private void pushToNetwork(Response response) {
+        exchange.respond(response);
+    }
+
+    private void logAndWrap(AdaptorException e) throws SessionException {
+        log.warn("Failed to convert msg: {}", e.getMessage(), e);
+        throw new SessionException(e);
+    }
+
+    @Override
+    public void onMsg(SessionCtrlMsg msg) throws SessionException {
+        log.debug("[{}] onCtrl: {}", sessionId, msg);
+        if (msg instanceof SessionCloseMsg) {
+            onSessionClose((SessionCloseMsg) msg);
+        }
+    }
+
+    private void onSessionClose(SessionCloseMsg msg) {
+        if (msg.isTimeout()) {
+            exchange.respond(ResponseCode.SERVICE_UNAVAILABLE);
+        } else {
+            exchange.respond(ResponseCode.INTERNAL_SERVER_ERROR);
+        }
+    }
+
+    @Override
+    public void onError(SessionException e) {
+        if (e instanceof SessionAuthException) {
+            log.warn("[{}] onError: {}", sessionId, e.getMessage());
+            exchange.respond(ResponseCode.UNAUTHORIZED);
+        } else {
+            log.warn("[{}] onError: {}", sessionId, e.getMessage(), e);
+            exchange.respond(ResponseCode.BAD_REQUEST);
+        }
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return sessionId;
+    }
+
+    @Override
+    public String toString() {
+        return "CoapSessionCtx [sessionId=" + sessionId + "]";
+    }
+
+    @Override
+    public boolean isClosed() {
+        return exchange.advanced().isComplete() || exchange.advanced().isTimedOut();
+    }
+
+    public void close() {
+        log.info("[{}] Closing processing context. Timeout: {}", sessionId, exchange.advanced().isTimedOut());
+        processor.process(new SessionCloseMsg(sessionId, exchange.advanced().isTimedOut()));
+    }
+
+    @Override
+    public long getTimeout() {
+        return timeout;
+    }
+
+    public void setSessionType(SessionType sessionType) {
+        this.sessionType = sessionType;
+    }
+
+    @Override
+    public SessionType getSessionType() {
+        return sessionType;
+    }
+
+    public int nextSeqNumber() {
+        return seqNumber.getAndIncrement();
+    }
+}
diff --git a/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java
new file mode 100644
index 0000000..d679056
--- /dev/null
+++ b/transport/coap/src/main/java/org/thingsboard/server/transport/coap/session/CoapSessionId.java
@@ -0,0 +1,77 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+public final class CoapSessionId implements SessionId {
+
+    private final String clientAddress;
+    private final int clientPort;
+    private final String token;
+
+    public CoapSessionId(String host, int port, String token) {
+        super();
+        this.clientAddress = host;
+        this.clientPort = port;
+        this.token = token;
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + ((clientAddress == null) ? 0 : clientAddress.hashCode());
+        result = prime * result + clientPort;
+        result = prime * result + ((token == null) ? 0 : token.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        CoapSessionId other = (CoapSessionId) obj;
+        if (clientAddress == null) {
+            if (other.clientAddress != null)
+                return false;
+        } else if (!clientAddress.equals(other.clientAddress))
+            return false;
+        if (clientPort != other.clientPort)
+            return false;
+        if (token == null) {
+            if (other.token != null)
+                return false;
+        } else if (!token.equals(other.token))
+            return false;
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        return "CoapSessionId [clientAddress=" + clientAddress + ", clientPort=" + clientPort + ", token=" + token + "]";
+    }
+
+    @Override
+    public String toUidStr() {
+        return clientAddress + ":" + clientPort + ":" + token;
+    }
+
+}
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
new file mode 100644
index 0000000..a2d6c25
--- /dev/null
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTest.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.californium.core.CoapClient;
+import org.eclipse.californium.core.CoapResponse;
+import org.eclipse.californium.core.coap.CoAP.ResponseCode;
+import org.eclipse.californium.core.coap.MediaTypeRegistry;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.id.CustomerId;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.common.data.kv.AttributeKvEntry;
+import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
+import org.thingsboard.server.common.data.kv.LongDataEntry;
+import org.thingsboard.server.common.data.kv.StringDataEntry;
+import org.thingsboard.server.common.data.security.DeviceCredentialsFilter;
+import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.aware.SessionAwareMsg;
+import org.thingsboard.server.common.msg.core.BasicGetAttributesResponse;
+import org.thingsboard.server.common.msg.core.BasicRequest;
+import org.thingsboard.server.common.msg.core.BasicStatusCodeResponse;
+import org.thingsboard.server.common.msg.kv.BasicAttributeKVMsg;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.session.ex.SessionAuthException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.ClassMode;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.thingsboard.server.common.transport.auth.DeviceAuthResult;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+@ContextConfiguration
+@DirtiesContext(classMode = ClassMode.BEFORE_CLASS)
+@Slf4j
+public class CoapServerTest {
+
+    private static final int TEST_PORT = 5555;
+    private static final String TELEMETRY_POST_MESSAGE = "[{\"key1\":\"value1\"}]";
+    private static final String TEST_ATTRIBUTES_RESPONSE = "{\"key1\":\"value1\",\"key2\":42}";
+    private static final String DEVICE1_TOKEN = "Device1Token";
+    private static final String DEVICE2_TOKEN = "Device2Token";
+
+    @Configuration
+    public static class EchoCoapServerITConfiguration extends CoapServerTestConfiguration {
+
+        @Bean
+        public static DeviceAuthService authService() {
+            return new DeviceAuthService() {
+
+                private final DeviceId devId = new DeviceId(UUID.randomUUID());
+
+                @Override
+                public DeviceAuthResult process(DeviceCredentialsFilter credentials) {
+                    if (credentials != null && credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
+                        DeviceTokenCredentials tokenCredentials = (DeviceTokenCredentials) credentials;
+                        if (tokenCredentials.getCredentialsId().equals(DEVICE1_TOKEN)) {
+                            return DeviceAuthResult.of(devId);
+                        }
+                    }
+                    return DeviceAuthResult.of("Credentials are invalid!");
+                }
+
+                @Override
+                public Optional<Device> findDeviceById(DeviceId deviceId) {
+                    if (deviceId.equals(devId)) {
+                        Device dev = new Device();
+                        dev.setId(devId);
+                        dev.setTenantId(new TenantId(UUID.randomUUID()));
+                        dev.setCustomerId(new CustomerId(UUID.randomUUID()));
+                        return Optional.of(dev);
+                    } else {
+                        return Optional.empty();
+                    }
+                }
+            };
+        }
+
+        @Bean
+        public static SessionMsgProcessor sessionMsgProcessor() {
+            return new SessionMsgProcessor() {
+
+                @Override
+                public void process(SessionAwareMsg toActorMsg) {
+                    if (toActorMsg instanceof ToDeviceActorSessionMsg) {
+                        AdaptorToSessionActorMsg sessionMsg = ((ToDeviceActorSessionMsg) toActorMsg).getSessionMsg();
+                        try {
+                            FromDeviceMsg deviceMsg = sessionMsg.getMsg();
+                            ToDeviceMsg toDeviceMsg = null;
+                            if (deviceMsg.getMsgType() == MsgType.POST_TELEMETRY_REQUEST) {
+                                toDeviceMsg = BasicStatusCodeResponse.onSuccess(deviceMsg.getMsgType(), BasicRequest.DEFAULT_REQUEST_ID);
+                            } else if (deviceMsg.getMsgType() == MsgType.GET_ATTRIBUTES_REQUEST) {
+                                List<AttributeKvEntry> data = new ArrayList<>();
+                                data.add(new BaseAttributeKvEntry(new StringDataEntry("key1", "value1"), System.currentTimeMillis()));
+                                data.add(new BaseAttributeKvEntry(new LongDataEntry("key2", 42L), System.currentTimeMillis()));
+                                BasicAttributeKVMsg kv = BasicAttributeKVMsg.fromClient(data);
+                                toDeviceMsg = BasicGetAttributesResponse.onSuccess(deviceMsg.getMsgType(), BasicRequest.DEFAULT_REQUEST_ID, kv);
+                            }
+                            if (toDeviceMsg != null) {
+                                sessionMsg.getSessionContext().onMsg(new BasicSessionActorToAdaptorMsg(sessionMsg.getSessionContext(), toDeviceMsg));
+                            }
+                        } catch (Exception e) {
+                            e.printStackTrace();
+                        }
+                    }
+                }
+            };
+        }
+    }
+
+    @Autowired
+    private CoapTransportService service;
+
+    @Before
+    public void beforeTest() {
+        log.info("Service info: {}", service.toString());
+    }
+
+    @Test
+    public void testBadJsonTelemetryPostRequest() {
+        CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.TELEMETRY.name().toLowerCase());
+        CoapResponse response = client.setTimeout(6000).post("test", MediaTypeRegistry.APPLICATION_JSON);
+        Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+        log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+    }
+
+    @Test
+    public void testNoCredentialsPostRequest() {
+        CoapClient client = new CoapClient(getBaseTestUrl());
+        CoapResponse response = client.setTimeout(6000).post(TELEMETRY_POST_MESSAGE, MediaTypeRegistry.APPLICATION_JSON);
+        Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+        log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+    }
+
+    @Test
+    public void testValidJsonTelemetryPostRequest() {
+        CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.TELEMETRY.name().toLowerCase());
+        CoapResponse response = client.setTimeout(6000).post(TELEMETRY_POST_MESSAGE, MediaTypeRegistry.APPLICATION_JSON);
+        Assert.assertEquals(ResponseCode.CREATED, response.getCode());
+        log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+    }
+
+    @Test
+    public void testNoCredentialsAttributesGetRequest() {
+        CoapClient client = new CoapClient("coap://localhost:5555/api/v1?keys=key1,key2");
+        CoapResponse response = client.setTimeout(6000).get();
+        Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+    }
+
+    @Test
+    public void testNoKeysAttributesGetRequest() {
+        CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.ATTRIBUTES.name().toLowerCase() + "?data=key1,key2");
+        CoapResponse response = client.setTimeout(6000).get();
+        Assert.assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+    }
+
+    @Test
+    public void testValidAttributesGetRequest() {
+        CoapClient client = new CoapClient(getBaseTestUrl() + DEVICE1_TOKEN + "/" + FeatureType.ATTRIBUTES.name().toLowerCase() + "?clientKeys=key1,key2");
+        CoapResponse response = client.setTimeout(6000).get();
+        Assert.assertEquals(ResponseCode.CONTENT, response.getCode());
+        Assert.assertEquals(TEST_ATTRIBUTES_RESPONSE, response.getResponseText());
+        log.info("Response: {}, {}", response.getCode(), response.getResponseText());
+    }
+
+    private String getBaseTestUrl() {
+        return "coap://localhost:" + TEST_PORT + "/api/v1/";
+    }
+
+}
diff --git a/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java
new file mode 100644
index 0000000..39bd6c5
--- /dev/null
+++ b/transport/coap/src/test/java/org/thingsboard/server/transport/coap/CoapServerTestConfiguration.java
@@ -0,0 +1,35 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.coap;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.test.context.TestPropertySource;
+
+@Configuration
+@ComponentScan({ "org.thingsboard.server.transport.coap" })
+@PropertySource("classpath:coap-transport-test.properties")
+public class CoapServerTestConfiguration {
+
+    @Bean
+    public static PropertySourcesPlaceholderConfigurer propertyConfig() {
+        return new PropertySourcesPlaceholderConfigurer();
+    }
+
+}
diff --git a/transport/coap/src/test/resources/coap-transport-test.properties b/transport/coap/src/test/resources/coap-transport-test.properties
new file mode 100644
index 0000000..469b034
--- /dev/null
+++ b/transport/coap/src/test/resources/coap-transport-test.properties
@@ -0,0 +1,4 @@
+coap.bind_address=0.0.0.0
+coap.bind_port=5555
+coap.adaptor=JsonCoapAdaptor
+coap.timeout=10000
\ No newline at end of file
diff --git a/transport/http/pom.xml b/transport/http/pom.xml
new file mode 100644
index 0000000..665a152
--- /dev/null
+++ b/transport/http/pom.xml
@@ -0,0 +1,81 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard.server</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>transport</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server.transport</groupId>
+    <artifactId>http</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard HTTP Transport</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>transport</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
new file mode 100644
index 0000000..e3e0666
--- /dev/null
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/DeviceApiController.java
@@ -0,0 +1,195 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.http;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicAdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.FromDeviceMsg;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.http.session.HttpSessionCtx;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Andrew Shvayka
+ */
+@RestController
+@RequestMapping("/api/v1")
+@Slf4j
+public class DeviceApiController {
+
+    @Value("${http.request_timeout}")
+    private long defaultTimeout;
+
+    @Autowired(required = false)
+    private SessionMsgProcessor processor;
+
+    @Autowired(required = false)
+    private DeviceAuthService authService;
+
+    @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.GET, produces = "application/json")
+    public DeferredResult<ResponseEntity> getDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
+                                                              @RequestParam(value = "clientKeys", required = false) String clientKeys,
+                                                              @RequestParam(value = "sharedKeys", required = false) String sharedKeys) {
+        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+        if (StringUtils.isEmpty(clientKeys) && StringUtils.isEmpty(sharedKeys)) {
+            responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+        } else {
+            HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+            if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+                Set<String> clientKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
+                Set<String> sharedKeySet = new HashSet<>(Arrays.asList(clientKeys.split(",")));
+                process(ctx, new BasicGetAttributesRequest(0, clientKeySet, sharedKeySet));
+            } else {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+            }
+        }
+
+        return responseWriter;
+    }
+
+    @RequestMapping(value = "/{deviceToken}/attributes", method = RequestMethod.POST)
+    public DeferredResult<ResponseEntity> postDeviceAttributes(@PathVariable("deviceToken") String deviceToken,
+                                                               @RequestBody String json) {
+        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+        HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+        if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+            try {
+                process(ctx, JsonConverter.convertToAttributes(new JsonParser().parse(json)));
+            } catch (IllegalStateException | JsonSyntaxException ex) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            }
+        } else {
+            responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+        }
+        return responseWriter;
+    }
+
+    @RequestMapping(value = "/{deviceToken}/telemetry", method = RequestMethod.POST)
+    public DeferredResult<ResponseEntity> postTelemetry(@PathVariable("deviceToken") String deviceToken,
+                                                        @RequestBody String json) {
+        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+        HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+        if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+            try {
+                process(ctx, JsonConverter.convertToTelemetry(new JsonParser().parse(json)));
+            } catch (IllegalStateException | JsonSyntaxException ex) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            }
+        } else {
+            responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+        }
+        return responseWriter;
+    }
+
+    @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.GET, produces = "application/json")
+    public DeferredResult<ResponseEntity> subscribeToCommands(@PathVariable("deviceToken") String deviceToken,
+                                                              @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) {
+        return subscribe(deviceToken, timeout, new RpcSubscribeMsg());
+    }
+
+    @RequestMapping(value = "/{deviceToken}/rpc/{requestId}", method = RequestMethod.POST)
+    public DeferredResult<ResponseEntity> replyToCommand(@PathVariable("deviceToken") String deviceToken,
+                                                         @PathVariable("requestId") Integer requestId,
+                                                         @RequestBody String json) {
+        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+        HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+        if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+            try {
+                JsonObject response = new JsonParser().parse(json).getAsJsonObject();
+                process(ctx, new ToDeviceRpcResponseMsg(requestId, response.toString()));
+            } catch (IllegalStateException | JsonSyntaxException ex) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            }
+        } else {
+            responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+        }
+        return responseWriter;
+    }
+
+    @RequestMapping(value = "/{deviceToken}/rpc", method = RequestMethod.POST)
+    public DeferredResult<ResponseEntity> postRpcRequest(@PathVariable("deviceToken") String deviceToken,
+                                                         @RequestBody String json) {
+        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+        HttpSessionCtx ctx = getHttpSessionCtx(responseWriter);
+        if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+            try {
+                JsonObject request = new JsonParser().parse(json).getAsJsonObject();
+                process(ctx, new ToServerRpcRequestMsg(0,
+                        request.get("method").getAsString(),
+                        request.get("params").toString()));
+            } catch (IllegalStateException | JsonSyntaxException ex) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            }
+        } else {
+            responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+        }
+        return responseWriter;
+    }
+
+    @RequestMapping(value = "/{deviceToken}/attributes/updates", method = RequestMethod.GET, produces = "application/json")
+    public DeferredResult<ResponseEntity> subscribeToAttributes(@PathVariable("deviceToken") String deviceToken,
+                                                                @RequestParam(value = "timeout", required = false, defaultValue = "0") long timeout) {
+        return subscribe(deviceToken, timeout, new AttributesSubscribeMsg());
+    }
+
+    private DeferredResult<ResponseEntity> subscribe(String deviceToken, long timeout, FromDeviceMsg msg) {
+        DeferredResult<ResponseEntity> responseWriter = new DeferredResult<ResponseEntity>();
+        HttpSessionCtx ctx = getHttpSessionCtx(responseWriter, timeout);
+        if (ctx.login(new DeviceTokenCredentials(deviceToken))) {
+            try {
+                process(ctx, msg);
+            } catch (IllegalStateException | JsonSyntaxException ex) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.BAD_REQUEST));
+            }
+        } else {
+            responseWriter.setResult(new ResponseEntity<>(HttpStatus.UNAUTHORIZED));
+        }
+        return responseWriter;
+    }
+
+    private HttpSessionCtx getHttpSessionCtx(DeferredResult<ResponseEntity> responseWriter) {
+        return getHttpSessionCtx(responseWriter, defaultTimeout);
+    }
+
+    private HttpSessionCtx getHttpSessionCtx(DeferredResult<ResponseEntity> responseWriter, long timeout) {
+        return new HttpSessionCtx(processor, authService, responseWriter, timeout != 0 ? timeout : defaultTimeout);
+    }
+
+    private void process(HttpSessionCtx ctx, FromDeviceMsg request) {
+        AdaptorToSessionActorMsg msg = new BasicAdaptorToSessionActorMsg(ctx, request);
+        processor.process(new BasicToDeviceActorSessionMsg(ctx.getDevice(), msg));
+    }
+
+}
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
new file mode 100644
index 0000000..60805a5
--- /dev/null
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionCtx.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.http.session;
+
+import com.google.gson.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+
+import java.util.function.Consumer;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class HttpSessionCtx extends DeviceAwareSessionContext {
+
+    private final SessionId sessionId;
+    private final long timeout;
+    private final DeferredResult<ResponseEntity> responseWriter;
+
+    public HttpSessionCtx(SessionMsgProcessor processor, DeviceAuthService authService, DeferredResult<ResponseEntity> responseWriter, long timeout) {
+        super(processor, authService);
+        this.sessionId = new HttpSessionId();
+        this.responseWriter = responseWriter;
+        this.timeout = timeout;
+    }
+
+    @Override
+    public SessionType getSessionType() {
+        return SessionType.SYNC;
+    }
+
+    @Override
+    public void onMsg(SessionActorToAdaptorMsg source) throws SessionException {
+        ToDeviceMsg msg = source.getMsg();
+        switch (msg.getMsgType()) {
+            case GET_ATTRIBUTES_RESPONSE:
+                reply((GetAttributesResponse) msg);
+                return;
+            case STATUS_CODE_RESPONSE:
+                reply((StatusCodeResponse) msg);
+                return;
+            case ATTRIBUTES_UPDATE_NOTIFICATION:
+                reply((AttributesUpdateNotification) msg);
+                return;
+            case TO_DEVICE_RPC_REQUEST:
+                reply((ToDeviceRpcRequestMsg) msg);
+                return;
+            case TO_SERVER_RPC_RESPONSE:
+                reply((ToServerRpcResponseMsg) msg);
+                return;
+            case RULE_ENGINE_ERROR:
+                reply((RuleEngineErrorMsg) msg);
+                return;
+        }
+    }
+
+    private void reply(RuleEngineErrorMsg msg) {
+        HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
+        switch (msg.getError()) {
+            case PLUGIN_TIMEOUT:
+                status = HttpStatus.REQUEST_TIMEOUT;
+                break;
+            default:
+                if (msg.getInMsgType() == MsgType.TO_SERVER_RPC_REQUEST) {
+                    status = HttpStatus.BAD_REQUEST;
+                }
+                break;
+        }
+        responseWriter.setResult(new ResponseEntity<>(JsonConverter.toErrorJson(msg.getErrorMsg()).toString(), status));
+    }
+
+    private <T> void reply(ResponseMsg<? extends T> msg, Consumer<T> f) {
+        if (!msg.getError().isPresent()) {
+            f.accept(msg.getData().get());
+        } else {
+            Exception e = msg.getError().get();
+            responseWriter.setResult(new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR));
+        }
+    }
+
+    private void reply(ToDeviceRpcRequestMsg msg) {
+        responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg, true).toString(), HttpStatus.OK));
+    }
+
+    private void reply(ToServerRpcResponseMsg msg) {
+        responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg).toString(), HttpStatus.OK));
+    }
+
+    private void reply(AttributesUpdateNotification msg) {
+        responseWriter.setResult(new ResponseEntity<>(JsonConverter.toJson(msg.getData(), false).toString(), HttpStatus.OK));
+    }
+
+    private void reply(GetAttributesResponse msg) {
+        reply(msg, payload -> {
+            if (payload.getClientAttributes().isEmpty() && payload.getSharedAttributes().isEmpty()) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.NOT_FOUND));
+            } else {
+                JsonObject result = JsonConverter.toJson(payload, false);
+                responseWriter.setResult(new ResponseEntity<>(result.toString(), HttpStatus.OK));
+            }
+        });
+    }
+
+    private void reply(StatusCodeResponse msg) {
+        reply(msg, payload -> {
+            if (payload == 0) {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.OK));
+            } else {
+                responseWriter.setResult(new ResponseEntity<>(HttpStatus.valueOf(payload)));
+            }
+        });
+    }
+
+    @Override
+    public void onMsg(SessionCtrlMsg msg) throws SessionException {
+
+    }
+
+    @Override
+    public void onError(SessionException e) {
+
+    }
+
+    @Override
+    public boolean isClosed() {
+        return false;
+    }
+
+    @Override
+    public long getTimeout() {
+        return timeout;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return sessionId;
+    }
+}
diff --git a/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java
new file mode 100644
index 0000000..9c9340e
--- /dev/null
+++ b/transport/http/src/main/java/org/thingsboard/server/transport/http/session/HttpSessionId.java
@@ -0,0 +1,37 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.http.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+import java.util.UUID;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class HttpSessionId implements SessionId {
+
+    private final UUID id;
+
+    public HttpSessionId() {
+        this.id = UUID.randomUUID();
+    }
+
+    @Override
+    public String toUidStr() {
+        return id.toString();
+    }
+}
diff --git a/transport/mqtt/pom.xml b/transport/mqtt/pom.xml
new file mode 100644
index 0000000..045bb1f
--- /dev/null
+++ b/transport/mqtt/pom.xml
@@ -0,0 +1,90 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard.server</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>transport</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server.transport</groupId>
+    <artifactId>mqtt</artifactId>
+    <packaging>jar</packaging>
+
+    <name>Thingsboard MQTT Transport</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <main.dir>${basedir}/../..</main.dir>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.thingsboard.server.common</groupId>
+            <artifactId>transport</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>io.netty</groupId>
+            <artifactId>netty-all</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+        </dependency>
+        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>18.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-all</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
new file mode 100644
index 0000000..5af244a
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/JsonMqttAdaptor.java
@@ -0,0 +1,238 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.adaptors;
+
+import com.google.gson.*;
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.buffer.UnpooledByteBufAllocator;
+import io.netty.handler.codec.mqtt.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.thingsboard.server.common.msg.core.*;
+import org.thingsboard.server.common.msg.kv.AttributesKVMsg;
+import org.thingsboard.server.common.msg.session.*;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.adaptor.JsonConverter;
+import org.thingsboard.server.transport.mqtt.MqttTransportHandler;
+import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
+
+import java.nio.charset.Charset;
+import java.util.*;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Component("JsonMqttAdaptor")
+@Slf4j
+public class JsonMqttAdaptor implements MqttTransportAdaptor {
+
+    private static final Gson GSON = new Gson();
+    private static final Charset UTF8 = Charset.forName("UTF-8");
+    private static final ByteBufAllocator ALLOCATOR = new UnpooledByteBufAllocator(false);
+
+    @Override
+    public AdaptorToSessionActorMsg convertToActorMsg(MqttSessionCtx ctx, MsgType type, MqttMessage inbound) throws AdaptorException {
+        FromDeviceMsg msg;
+        switch (type) {
+            case POST_TELEMETRY_REQUEST:
+                msg = convertToTelemetryUploadRequest(ctx, (MqttPublishMessage) inbound);
+                break;
+            case POST_ATTRIBUTES_REQUEST:
+                msg = convertToUpdateAttributesRequest(ctx, (MqttPublishMessage) inbound);
+                break;
+            case SUBSCRIBE_ATTRIBUTES_REQUEST:
+                msg = new AttributesSubscribeMsg();
+                break;
+            case UNSUBSCRIBE_ATTRIBUTES_REQUEST:
+                msg = new AttributesUnsubscribeMsg();
+                break;
+            case SUBSCRIBE_RPC_COMMANDS_REQUEST:
+                msg = new RpcSubscribeMsg();
+                break;
+            case UNSUBSCRIBE_RPC_COMMANDS_REQUEST:
+                msg = new RpcUnsubscribeMsg();
+                break;
+            case GET_ATTRIBUTES_REQUEST:
+                msg = convertToGetAttributesRequest(ctx, (MqttPublishMessage) inbound);
+                break;
+            case TO_DEVICE_RPC_RESPONSE:
+                msg = convertToRpcCommandResponse(ctx, (MqttPublishMessage) inbound);
+                break;
+            case TO_SERVER_RPC_REQUEST:
+                msg = convertToServerRpcRequest(ctx, (MqttPublishMessage) inbound);
+                break;
+            default:
+                log.warn("[{}] Unsupported msg type: {}!", ctx.getSessionId(), type);
+                throw new AdaptorException(new IllegalArgumentException("Unsupported msg type: " + type + "!"));
+        }
+        return new BasicAdaptorToSessionActorMsg(ctx, msg);
+    }
+
+    @Override
+    public Optional<MqttMessage> convertToAdaptorMsg(MqttSessionCtx ctx, SessionActorToAdaptorMsg sessionMsg) throws AdaptorException {
+        MqttMessage result = null;
+        ToDeviceMsg msg = sessionMsg.getMsg();
+        switch (msg.getMsgType()) {
+            case STATUS_CODE_RESPONSE:
+            case GET_ATTRIBUTES_RESPONSE:
+                ResponseMsg<?> responseMsg = (ResponseMsg) msg;
+                if (responseMsg.isSuccess()) {
+                    MsgType requestMsgType = responseMsg.getRequestMsgType();
+                    Integer requestId = responseMsg.getRequestId();
+                    if (requestId >= 0) {
+                        if (requestMsgType == MsgType.POST_ATTRIBUTES_REQUEST || requestMsgType == MsgType.POST_TELEMETRY_REQUEST) {
+                            result = MqttTransportHandler.createMqttPubAckMsg(requestId);
+                        } else if (requestMsgType == MsgType.GET_ATTRIBUTES_REQUEST) {
+                            GetAttributesResponse response = (GetAttributesResponse) msg;
+                            if (response.isSuccess()) {
+                                result = createMqttPublishMsg(ctx,
+                                        MqttTransportHandler.ATTRIBUTES_RESPONSE_TOPIC_PREFIX + requestId,
+                                        response.getData().get(), true);
+                            } else {
+                                throw new AdaptorException(response.getError().get());
+                            }
+                        }
+                    }
+                } else {
+                    if (responseMsg.getError().isPresent()) {
+                        throw new AdaptorException(responseMsg.getError().get());
+                    }
+                }
+                break;
+            case ATTRIBUTES_UPDATE_NOTIFICATION:
+                AttributesUpdateNotification notification = (AttributesUpdateNotification) msg;
+                result = createMqttPublishMsg(ctx, MqttTransportHandler.ATTRIBUTES_TOPIC, notification.getData(), false);
+                break;
+            case TO_DEVICE_RPC_REQUEST:
+                ToDeviceRpcRequestMsg rpcRequest = (ToDeviceRpcRequestMsg) msg;
+                result = createMqttPublishMsg(ctx, MqttTransportHandler.RPC_REQUESTS_TOPIC + rpcRequest.getRequestId(),
+                        rpcRequest);
+                break;
+            case TO_SERVER_RPC_RESPONSE:
+                ToServerRpcResponseMsg rpcResponse = (ToServerRpcResponseMsg) msg;
+                result = createMqttPublishMsg(ctx, MqttTransportHandler.RPC_REQUESTS_TOPIC + rpcResponse.getRequestId(),
+                        rpcResponse);
+                break;
+            case RULE_ENGINE_ERROR:
+                RuleEngineErrorMsg errorMsg = (RuleEngineErrorMsg) msg;
+                result = createMqttPublishMsg(ctx, "errors", JsonConverter.toErrorJson(errorMsg.getErrorMsg()));
+                break;
+        }
+        return Optional.ofNullable(result);
+    }
+
+    private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, AttributesKVMsg msg, boolean asMap) {
+        return createMqttPublishMsg(ctx, topic, JsonConverter.toJson(msg, asMap));
+    }
+
+    private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, ToDeviceRpcRequestMsg msg) {
+        return createMqttPublishMsg(ctx, topic, JsonConverter.toJson(msg, false));
+    }
+
+    private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, ToServerRpcResponseMsg msg) {
+        return createMqttPublishMsg(ctx, topic, JsonConverter.toJson(msg));
+    }
+
+    private MqttPublishMessage createMqttPublishMsg(MqttSessionCtx ctx, String topic, JsonElement json) {
+        MqttFixedHeader mqttFixedHeader =
+                new MqttFixedHeader(MqttMessageType.PUBLISH, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+        MqttPublishVariableHeader header = new MqttPublishVariableHeader(topic, ctx.nextMsgId());
+        ByteBuf payload = ALLOCATOR.buffer();
+        payload.writeBytes(GSON.toJson(json).getBytes(UTF8));
+        return new MqttPublishMessage(mqttFixedHeader, header, payload);
+    }
+
+    private FromDeviceMsg convertToGetAttributesRequest(MqttSessionCtx ctx, MqttPublishMessage inbound) throws AdaptorException {
+        String topicName = inbound.variableHeader().topicName();
+        try {
+            Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.ATTRIBUTES_REQUEST_TOPIC_PREFIX.length()));
+            String payload = inbound.payload().toString(UTF8);
+            JsonElement requestBody = new JsonParser().parse(payload);
+            return new BasicGetAttributesRequest(requestId,
+                    toStringSet(requestBody, "clientKeys"), toStringSet(requestBody, "sharedKeys"));
+        } catch (RuntimeException e) {
+            log.warn("Failed to decode get attributes request", e);
+            throw new AdaptorException(e);
+        }
+    }
+
+    private FromDeviceMsg convertToRpcCommandResponse(MqttSessionCtx ctx, MqttPublishMessage inbound) throws AdaptorException {
+        String topicName = inbound.variableHeader().topicName();
+        try {
+            Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.RPC_RESPONSE_TOPIC.length()));
+            String payload = inbound.payload().toString(UTF8);
+            return new ToDeviceRpcResponseMsg(
+                    requestId,
+                    payload);
+        } catch (RuntimeException e) {
+            log.warn("Failed to decode get attributes request", e);
+            throw new AdaptorException(e);
+        }
+    }
+
+    private Set<String> toStringSet(JsonElement requestBody, String name) {
+        JsonElement element = requestBody.getAsJsonObject().get(name);
+        if (element != null) {
+            return new HashSet<>(Arrays.asList(element.getAsString().split(",")));
+        } else {
+            return Collections.emptySet();
+        }
+    }
+
+    private UpdateAttributesRequest convertToUpdateAttributesRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
+        String payload = validatePayload(ctx, inbound.payload());
+        try {
+            return JsonConverter.convertToAttributes(new JsonParser().parse(payload), inbound.variableHeader().messageId());
+        } catch (IllegalStateException | JsonSyntaxException ex) {
+            throw new AdaptorException(ex);
+        }
+    }
+
+    private TelemetryUploadRequest convertToTelemetryUploadRequest(SessionContext ctx, MqttPublishMessage inbound) throws AdaptorException {
+        String payload = validatePayload(ctx, inbound.payload());
+        try {
+            return JsonConverter.convertToTelemetry(new JsonParser().parse(payload), inbound.variableHeader().messageId());
+        } catch (IllegalStateException | JsonSyntaxException ex) {
+            throw new AdaptorException(ex);
+        }
+    }
+
+    private FromDeviceMsg convertToServerRpcRequest(MqttSessionCtx ctx, MqttPublishMessage inbound) throws AdaptorException {
+        String topicName = inbound.variableHeader().topicName();
+        String payload = validatePayload(ctx, inbound.payload());
+        try {
+            Integer requestId = Integer.valueOf(topicName.substring(MqttTransportHandler.RPC_REQUESTS_TOPIC.length()));
+            return JsonConverter.convertToServerRpcRequest(new JsonParser().parse(payload), requestId);
+        } catch (IllegalStateException | JsonSyntaxException ex) {
+            throw new AdaptorException(ex);
+        }
+    }
+
+    private String validatePayload(SessionContext ctx, ByteBuf payloadData) throws AdaptorException {
+        try {
+            String payload = payloadData.toString(UTF8);
+            if (payload == null) {
+                log.warn("[{}] Payload is empty!", ctx.getSessionId());
+                throw new AdaptorException(new IllegalArgumentException("Payload is empty!"));
+            }
+            return payload;
+        } finally {
+            payloadData.release();
+        }
+    }
+
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
new file mode 100644
index 0000000..32878ad
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/adaptors/MqttTransportAdaptor.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.adaptors;
+
+import io.netty.handler.codec.mqtt.MqttMessage;
+import org.thingsboard.server.common.transport.TransportAdaptor;
+import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
+
+/**
+ * @author Andrew Shvayka
+ */
+public interface MqttTransportAdaptor extends TransportAdaptor<MqttSessionCtx, MqttMessage, MqttMessage> {
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
new file mode 100644
index 0000000..f7b38d0
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttSslHandlerProvider.java
@@ -0,0 +1,91 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import com.google.common.io.Resources;
+import io.netty.handler.ssl.SslHandler;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import javax.net.ssl.*;
+import java.io.File;
+import java.io.FileInputStream;
+import java.net.URL;
+import java.security.KeyStore;
+
+/**
+ * Created by valerii.sosliuk on 11/6/16.
+ */
+@Slf4j
+@Component("MqttSslHandlerProvider")
+@ConditionalOnProperty(prefix = "mqtt.ssl", value = "key-store", havingValue = "", matchIfMissing = false)
+public class MqttSslHandlerProvider {
+
+    public static final String TLS = "TLS";
+    @Value("${mqtt.ssl.key-store}")
+    private String keyStoreFile;
+    @Value("${mqtt.ssl.key-store-password}")
+    private String keyStorePassword;
+    @Value("${mqtt.ssl.keyStoreType}")
+    private String keyStoreType;
+
+    @Value("${mqtt.ssl.trust-store}")
+    private String trustStoreFile;
+    @Value("${mqtt.ssl.trust-store-password}")
+    private String trustStorePassword;
+    @Value("${mqtt.ssl.trustStoreType}")
+    private String trustStoreType;
+
+
+    public SslHandler getSslHandler() {
+        try {
+            URL ksUrl = Resources.getResource(keyStoreFile);
+            File ksFile = new File(ksUrl.toURI());
+            URL tsUrl = Resources.getResource(trustStoreFile);
+            File tsFile = new File(tsUrl.toURI());
+
+            TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            KeyStore trustStore = KeyStore.getInstance(trustStoreType);
+            trustStore.load(new FileInputStream(tsFile), trustStorePassword.toCharArray());
+            tmFactory.init(trustStore);
+
+            KeyStore ks = KeyStore.getInstance(keyStoreType);
+
+            ks.load(new FileInputStream(ksFile), keyStorePassword.toCharArray());
+            KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(ks, keyStorePassword.toCharArray());
+
+            KeyManager[] km = kmf.getKeyManagers();
+            TrustManager[] tm = tmFactory.getTrustManagers();
+            SSLContext sslContext = SSLContext.getInstance(TLS);
+            sslContext.init(km, tm, null);
+            SSLEngine sslEngine = sslContext.createSSLEngine();
+            sslEngine.setUseClientMode(false);
+            sslEngine.setNeedClientAuth(false);
+            sslEngine.setWantClientAuth(false);
+            sslEngine.setEnabledProtocols(sslEngine.getSupportedProtocols());
+            sslEngine.setEnabledCipherSuites(sslEngine.getSupportedCipherSuites());
+            sslEngine.setEnableSessionCreation(true);
+            return new SslHandler(sslEngine);
+        } catch (Exception e) {
+            log.error("Unable to set up SSL context. Reason: " + e.getMessage(), e);
+            throw new RuntimeException("Failed to get SSL handler", e);
+        }
+    }
+
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
new file mode 100644
index 0000000..e1bb45c
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java
@@ -0,0 +1,260 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInboundHandlerAdapter;
+import io.netty.handler.codec.mqtt.*;
+import io.netty.util.concurrent.Future;
+import io.netty.util.concurrent.GenericFutureListener;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.security.DeviceTokenCredentials;
+import org.thingsboard.server.common.msg.session.AdaptorToSessionActorMsg;
+import org.thingsboard.server.common.msg.session.BasicToDeviceActorSessionMsg;
+import org.thingsboard.server.common.msg.session.MsgType;
+import org.thingsboard.server.common.msg.session.ctrl.SessionCloseMsg;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+import org.thingsboard.server.transport.mqtt.session.MqttSessionCtx;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttTransportHandler extends ChannelInboundHandlerAdapter implements GenericFutureListener<Future<? super Void>> {
+
+    public static final MqttQoS MAX_SUPPORTED_QOS_LVL = MqttQoS.AT_LEAST_ONCE;
+    public static final String BASE_TOPIC = "v1/devices/me";
+    public static final String ATTRIBUTES_TOPIC = BASE_TOPIC + "/attributes";
+    public static final String TELEMETRY_TOPIC = BASE_TOPIC + "/telemetry";
+    public static final String ATTRIBUTES_REQUEST_TOPIC_PREFIX = BASE_TOPIC + "/attributes/request/";
+    public static final String ATTRIBUTES_RESPONSE_TOPIC_PREFIX = BASE_TOPIC + "/attributes/response/";
+    public static final String ATTRIBUTES_RESPONSES_TOPIC = ATTRIBUTES_RESPONSE_TOPIC_PREFIX + "+";
+    public static final String RPC_REQUESTS_TOPIC = BASE_TOPIC + "/rpc/request/";
+    public static final String RPC_REQUESTS_SUB_TOPIC = RPC_REQUESTS_TOPIC + "+";
+    public static final String RPC_RESPONSE_TOPIC = BASE_TOPIC + "/rpc/response/";
+    public static final String RPC_RESPONSE_SUB_TOPIC = RPC_RESPONSE_TOPIC + "+";
+    private final MqttSessionCtx sessionCtx;
+    private final String sessionId;
+    private final MqttTransportAdaptor adaptor;
+    private final SessionMsgProcessor processor;
+
+    public MqttTransportHandler(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) {
+        this.processor = processor;
+        this.adaptor = adaptor;
+        this.sessionCtx = new MqttSessionCtx(processor, authService, adaptor);
+        this.sessionId = sessionCtx.getSessionId().toUidStr();
+    }
+
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) {
+        log.trace("[{}] Processing msg: {}", sessionId, msg);
+        if (msg instanceof MqttMessage) {
+            processMqttMsg(ctx, (MqttMessage) msg);
+        }
+    }
+
+    private void processMqttMsg(ChannelHandlerContext ctx, MqttMessage msg) {
+        sessionCtx.setChannel(ctx);
+        switch (msg.fixedHeader().messageType()) {
+            case CONNECT:
+                processConnect(ctx, (MqttConnectMessage) msg);
+                break;
+            case PUBLISH:
+                processPublish(ctx, (MqttPublishMessage) msg);
+                break;
+            case SUBSCRIBE:
+                processSubscribe(ctx, (MqttSubscribeMessage) msg);
+                break;
+            case UNSUBSCRIBE:
+                processUnsubscribe(ctx, (MqttUnsubscribeMessage) msg);
+                break;
+            case PINGREQ:
+                ctx.writeAndFlush(new MqttMessage(new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0)));
+                break;
+            case DISCONNECT:
+                processDisconnect(ctx);
+                break;
+        }
+    }
+
+    private void processPublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg) {
+        String topicName = mqttMsg.variableHeader().topicName();
+        int msgId = mqttMsg.variableHeader().messageId();
+        log.trace("[{}] Processing publish msg [{}][{}]!", sessionId, topicName, msgId);
+        AdaptorToSessionActorMsg msg = null;
+        try {
+            if (topicName.equals(ATTRIBUTES_TOPIC)) {
+                msg = adaptor.convertToActorMsg(sessionCtx, MsgType.POST_ATTRIBUTES_REQUEST, mqttMsg);
+            } else if (topicName.equals(TELEMETRY_TOPIC)) {
+                msg = adaptor.convertToActorMsg(sessionCtx, MsgType.POST_TELEMETRY_REQUEST, mqttMsg);
+            } else if (topicName.startsWith(ATTRIBUTES_REQUEST_TOPIC_PREFIX)) {
+                msg = adaptor.convertToActorMsg(sessionCtx, MsgType.GET_ATTRIBUTES_REQUEST, mqttMsg);
+                if (msgId >= 0) {
+                    ctx.writeAndFlush(createMqttPubAckMsg(msgId));
+                }
+            } else if (topicName.startsWith(RPC_RESPONSE_TOPIC)) {
+                msg = adaptor.convertToActorMsg(sessionCtx, MsgType.TO_DEVICE_RPC_RESPONSE, mqttMsg);
+                if (msgId >= 0) {
+                    ctx.writeAndFlush(createMqttPubAckMsg(msgId));
+                }
+            } else if (topicName.startsWith(RPC_REQUESTS_TOPIC)) {
+                msg = adaptor.convertToActorMsg(sessionCtx, MsgType.TO_SERVER_RPC_REQUEST, mqttMsg);
+                if (msgId >= 0) {
+                    ctx.writeAndFlush(createMqttPubAckMsg(msgId));
+                }
+            }
+        } catch (AdaptorException e) {
+            log.warn("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e);
+        }
+
+        if (msg != null) {
+            processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+        } else {
+            log.warn("[{}] Closing current session due to invalid publish msg [{}][{}]", sessionId, topicName, msgId);
+            ctx.close();
+        }
+    }
+
+    private void processSubscribe(ChannelHandlerContext ctx, MqttSubscribeMessage mqttMsg) {
+        log.info("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId());
+        List<Integer> grantedQoSList = new ArrayList<>();
+        for (MqttTopicSubscription subscription : mqttMsg.payload().topicSubscriptions()) {
+            String topicName = subscription.topicName();
+            //TODO: handle this qos level.
+            MqttQoS reqQoS = subscription.qualityOfService();
+            try {
+                if (topicName.equals(ATTRIBUTES_TOPIC)) {
+                    AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.SUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
+                    processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+                    grantedQoSList.add(getMinSupportedQos(reqQoS));
+                } else if (topicName.equals(RPC_REQUESTS_SUB_TOPIC)) {
+                    AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.SUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
+                    processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+                    grantedQoSList.add(getMinSupportedQos(reqQoS));
+                } else if (topicName.equals(RPC_RESPONSE_SUB_TOPIC)) {
+                    grantedQoSList.add(getMinSupportedQos(reqQoS));
+                } else if (topicName.equals(ATTRIBUTES_RESPONSES_TOPIC)) {
+                    sessionCtx.setAllowAttributeResponses();
+                    grantedQoSList.add(getMinSupportedQos(reqQoS));
+                } else {
+                    log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
+                    grantedQoSList.add(MqttQoS.FAILURE.value());
+                }
+            } catch (AdaptorException e) {
+                log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topicName, reqQoS);
+                grantedQoSList.add(MqttQoS.FAILURE.value());
+            }
+        }
+        ctx.writeAndFlush(createSubAckMessage(mqttMsg.variableHeader().messageId(), grantedQoSList));
+    }
+
+    private void processUnsubscribe(ChannelHandlerContext ctx, MqttUnsubscribeMessage mqttMsg) {
+        log.info("[{}] Processing subscription [{}]!", sessionId, mqttMsg.variableHeader().messageId());
+        for (String topicName : mqttMsg.payload().topics()) {
+            try {
+                if (topicName.equals(ATTRIBUTES_TOPIC)) {
+                    AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.UNSUBSCRIBE_ATTRIBUTES_REQUEST, mqttMsg);
+                    processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+                } else if (topicName.equals(RPC_REQUESTS_SUB_TOPIC)) {
+                    AdaptorToSessionActorMsg msg = adaptor.convertToActorMsg(sessionCtx, MsgType.UNSUBSCRIBE_RPC_COMMANDS_REQUEST, mqttMsg);
+                    processor.process(new BasicToDeviceActorSessionMsg(sessionCtx.getDevice(), msg));
+                } else if (topicName.equals(ATTRIBUTES_RESPONSES_TOPIC)) {
+                    sessionCtx.setDisallowAttributeResponses();
+                }
+            } catch (AdaptorException e) {
+                log.warn("[{}] Failed to process unsubscription [{}] to [{}]", sessionId, mqttMsg.variableHeader().messageId(), topicName);
+            }
+        }
+        ctx.writeAndFlush(createUnSubAckMessage(mqttMsg.variableHeader().messageId()));
+    }
+
+    private MqttMessage createUnSubAckMessage(int msgId) {
+        MqttFixedHeader mqttFixedHeader =
+                new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+        MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
+        return new MqttMessage(mqttFixedHeader, mqttMessageIdVariableHeader);
+    }
+
+    private void processConnect(ChannelHandlerContext ctx, MqttConnectMessage msg) {
+        log.info("[{}] Processing connect msg for client: {}!", sessionId, msg.payload().clientIdentifier());
+        String userName = msg.payload().userName();
+        if (StringUtils.isEmpty(userName)) {
+            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD));
+            ctx.close();
+        } else if (sessionCtx.login(new DeviceTokenCredentials(msg.payload().userName()))) {
+            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_ACCEPTED));
+        } else {
+            ctx.writeAndFlush(createMqttConnAckMsg(MqttConnectReturnCode.CONNECTION_REFUSED_NOT_AUTHORIZED));
+            ctx.close();
+        }
+    }
+
+    private void processDisconnect(ChannelHandlerContext ctx) {
+        processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+        ctx.close();
+    }
+
+    private MqttConnAckMessage createMqttConnAckMsg(MqttConnectReturnCode returnCode) {
+        MqttFixedHeader mqttFixedHeader =
+                new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0);
+        MqttConnAckVariableHeader mqttConnAckVariableHeader =
+                new MqttConnAckVariableHeader(returnCode, true);
+        return new MqttConnAckMessage(mqttFixedHeader, mqttConnAckVariableHeader);
+    }
+
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) {
+        ctx.flush();
+    }
+
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+        log.error("[{}] Unexpected Exception", sessionId, cause);
+        ctx.close();
+    }
+
+    private static MqttSubAckMessage createSubAckMessage(Integer msgId, List<Integer> grantedQoSList) {
+        MqttFixedHeader mqttFixedHeader =
+                new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+        MqttMessageIdVariableHeader mqttMessageIdVariableHeader = MqttMessageIdVariableHeader.from(msgId);
+        MqttSubAckPayload mqttSubAckPayload = new MqttSubAckPayload(grantedQoSList);
+        return new MqttSubAckMessage(mqttFixedHeader, mqttMessageIdVariableHeader, mqttSubAckPayload);
+    }
+
+    private static int getMinSupportedQos(MqttQoS reqQoS) {
+        return Math.min(reqQoS.value(), MAX_SUPPORTED_QOS_LVL.value());
+    }
+
+    public static MqttPubAckMessage createMqttPubAckMsg(int requestId) {
+        MqttFixedHeader mqttFixedHeader =
+                new MqttFixedHeader(MqttMessageType.PUBACK, false, MqttQoS.AT_LEAST_ONCE, false, 0);
+        MqttMessageIdVariableHeader mqttMsgIdVariableHeader =
+                MqttMessageIdVariableHeader.from(requestId);
+        return new MqttPubAckMessage(mqttFixedHeader, mqttMsgIdVariableHeader);
+    }
+
+    @Override
+    public void operationComplete(Future<? super Void> future) throws Exception {
+        processor.process(new SessionCloseMsg(sessionCtx.getSessionId(), false));
+    }
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
new file mode 100644
index 0000000..0c60309
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportServerInitializer.java
@@ -0,0 +1,69 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPipeline;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.handler.codec.mqtt.MqttDecoder;
+import io.netty.handler.codec.mqtt.MqttEncoder;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.util.SelfSignedCertificate;
+import org.springframework.beans.factory.annotation.Value;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+
+import javax.net.ssl.SSLException;
+import java.security.cert.CertificateException;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class MqttTransportServerInitializer extends ChannelInitializer<SocketChannel> {
+
+    private final SessionMsgProcessor processor;
+    private final DeviceAuthService authService;
+    private final MqttTransportAdaptor adaptor;
+    private final MqttSslHandlerProvider sslHandlerProvider;
+
+    public MqttTransportServerInitializer(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor,
+                                          MqttSslHandlerProvider sslHandlerProvider) {
+        this.processor = processor;
+        this.authService = authService;
+        this.adaptor = adaptor;
+        this.sslHandlerProvider = sslHandlerProvider;
+    }
+
+    @Override
+    public void initChannel(SocketChannel ch) {
+        ChannelPipeline pipeline = ch.pipeline();
+        if (sslHandlerProvider != null) {
+            pipeline.addLast(sslHandlerProvider.getSslHandler());
+        }
+        pipeline.addLast("decoder", new MqttDecoder());
+        pipeline.addLast("encoder", MqttEncoder.INSTANCE);
+
+        MqttTransportHandler handler = new MqttTransportHandler(processor, authService, adaptor);
+        pipeline.addLast(handler);
+        ch.closeFuture().addListener(handler);
+    }
+
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
new file mode 100644
index 0000000..e8569cf
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportService.java
@@ -0,0 +1,108 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt;
+
+import io.netty.bootstrap.ServerBootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandler;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioServerSocketChannel;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.util.ResourceLeakDetector;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationContext;
+import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import javax.net.ssl.SSLEngine;
+import java.util.concurrent.Executor;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Service("MqttTransportService")
+@Slf4j
+public class MqttTransportService {
+
+    private static final String V1 = "v1";
+    private static final String DEVICE = "device";
+
+    @Autowired(required = false)
+    private ApplicationContext appContext;
+
+    @Autowired(required = false)
+    private SessionMsgProcessor processor;
+
+    @Autowired(required = false)
+    private DeviceAuthService authService;
+
+    @Autowired(required = false)
+    private MqttSslHandlerProvider sslHandlerProvider;
+
+    @Value("${mqtt.bind_address}")
+    private String host;
+    @Value("${mqtt.bind_port}")
+    private Integer port;
+    @Value("${mqtt.adaptor}")
+    private String adaptorName;
+
+    private MqttTransportAdaptor adaptor;
+
+    private Channel serverChannel;
+    private EventLoopGroup bossGroup;
+    private EventLoopGroup workerGroup;
+
+    @PostConstruct
+    public void init() throws Exception {
+        ResourceLeakDetector.setLevel(ResourceLeakDetector.Level.ADVANCED);
+        log.info("Starting MQTT transport...");
+        log.info("Lookup MQTT transport adaptor {}", adaptorName);
+        this.adaptor = (MqttTransportAdaptor) appContext.getBean(adaptorName);
+
+        log.info("Starting MQTT transport server");
+        bossGroup = new NioEventLoopGroup(1);
+        workerGroup = new NioEventLoopGroup();
+        ServerBootstrap b = new ServerBootstrap();
+        b.group(bossGroup, workerGroup)
+                .channel(NioServerSocketChannel.class)
+                .handler(new LoggingHandler(LogLevel.TRACE))
+                .childHandler(new MqttTransportServerInitializer(processor, authService, adaptor, sslHandlerProvider));
+
+        serverChannel = b.bind(host, port).sync().channel();
+        log.info("Mqtt transport started!");
+    }
+
+    @PreDestroy
+    public void shutdown() throws InterruptedException {
+        log.info("Stopping MQTT transport!");
+        try {
+            serverChannel.close().sync();
+        } finally {
+            bossGroup.shutdownGracefully();
+            workerGroup.shutdownGracefully();
+        }
+        log.info("MQTT transport stopped!");
+    }
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
new file mode 100644
index 0000000..5cae9f5
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionCtx.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.session;
+
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.handler.codec.mqtt.MqttMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.SessionId;
+import org.thingsboard.server.common.msg.session.SessionActorToAdaptorMsg;
+import org.thingsboard.server.common.msg.session.SessionCtrlMsg;
+import org.thingsboard.server.common.msg.session.SessionType;
+import org.thingsboard.server.common.msg.session.ex.SessionException;
+import org.thingsboard.server.common.transport.SessionMsgProcessor;
+import org.thingsboard.server.common.transport.adaptor.AdaptorException;
+import org.thingsboard.server.common.transport.auth.DeviceAuthService;
+import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext;
+import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * @author Andrew Shvayka
+ */
+@Slf4j
+public class MqttSessionCtx extends DeviceAwareSessionContext {
+
+    private final MqttTransportAdaptor adaptor;
+    private final MqttSessionId sessionId;
+    private ChannelHandlerContext channel;
+    private volatile boolean allowAttributeResponses;
+    private AtomicInteger msgIdSeq = new AtomicInteger(0);
+
+    public MqttSessionCtx(SessionMsgProcessor processor, DeviceAuthService authService, MqttTransportAdaptor adaptor) {
+        super(processor, authService);
+        this.adaptor = adaptor;
+        this.sessionId = new MqttSessionId();
+    }
+
+    @Override
+    public SessionType getSessionType() {
+        return SessionType.ASYNC;
+    }
+
+    @Override
+    public void onMsg(SessionActorToAdaptorMsg msg) throws SessionException {
+        try {
+            adaptor.convertToAdaptorMsg(this, msg).ifPresent(this::pushToNetwork);
+        } catch (AdaptorException e) {
+            //TODO: close channel with disconnect;
+            logAndWrap(e);
+        }
+    }
+
+    private void logAndWrap(AdaptorException e) throws SessionException {
+        log.warn("Failed to convert msg: {}", e.getMessage(), e);
+        throw new SessionException(e);
+    }
+
+    private void pushToNetwork(MqttMessage msg) {
+        channel.writeAndFlush(msg);
+    }
+
+    @Override
+    public void onMsg(SessionCtrlMsg msg) throws SessionException {
+
+    }
+
+    @Override
+    public void onError(SessionException e) {
+
+    }
+
+    @Override
+    public boolean isClosed() {
+        return false;
+    }
+
+    @Override
+    public long getTimeout() {
+        return 0;
+    }
+
+    @Override
+    public SessionId getSessionId() {
+        return sessionId;
+    }
+
+    public void setChannel(ChannelHandlerContext channel) {
+        this.channel = channel;
+    }
+
+    public void setAllowAttributeResponses() {
+        allowAttributeResponses = true;
+    }
+
+    public void setDisallowAttributeResponses() {
+        allowAttributeResponses = false;
+    }
+
+    public int nextMsgId() {
+        return msgIdSeq.incrementAndGet();
+    }
+}
diff --git a/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java
new file mode 100644
index 0000000..79b4d77
--- /dev/null
+++ b/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttSessionId.java
@@ -0,0 +1,56 @@
+/**
+ * Copyright © 2016 The Thingsboard Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.thingsboard.server.transport.mqtt.session;
+
+import org.thingsboard.server.common.data.id.SessionId;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * @author Andrew Shvayka
+ */
+public class MqttSessionId implements SessionId {
+
+    private static final AtomicLong idSeq = new AtomicLong();
+
+    private final long id;
+
+    public MqttSessionId() {
+        this.id = idSeq.incrementAndGet();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        MqttSessionId that = (MqttSessionId) o;
+
+        return id == that.id;
+
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (id ^ (id >>> 32));
+    }
+
+    @Override
+    public String toUidStr() {
+        return "mqtt" + id;
+    }
+}

transport/pom.xml 50(+50 -0)

diff --git a/transport/pom.xml b/transport/pom.xml
new file mode 100644
index 0000000..4675cdf
--- /dev/null
+++ b/transport/pom.xml
@@ -0,0 +1,50 @@
+<!--
+
+    Copyright © 2016 The Thingsboard Authors
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.thingsboard</groupId>
+        <version>0.0.1-SNAPSHOT</version>
+        <artifactId>server</artifactId>
+    </parent>
+    <groupId>org.thingsboard.server</groupId>
+    <artifactId>transport</artifactId>
+    <packaging>pom</packaging>
+
+    <name>Thingsboard Server Transport Modules</name>
+    <url>http://thingsboard.org</url>
+
+    <properties>
+        <main.dir>${basedir}/..</main.dir>
+    </properties>
+
+    <modules>
+        <module>http</module>
+        <module>coap</module>
+        <module>mqtt</module>
+    </modules>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>