diff --git a/CMakeLists.txt b/CMakeLists.txt
index ac657f10407f1a6ce66f2543cb47418709538dd1..7703869c258de2efcc9a6ba5b20dad79ca653dea 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -82,6 +82,11 @@ find_library(UUID_LIBRARIES NAMES uuid libuuid)
 else()
 endif()
 
+# Check for optional opencv components
+set(CMAKE_REQUIRED_INCLUDES ${OpenCV_INCLUDE_DIRS})
+check_include_file_cxx("opencv2/viz.hpp" HAVE_VIZ)
+check_include_file_cxx("opencv2/cudastereo.hpp" HAVE_OPENCVCUDA)
+
 # Optional source problem check
 find_program(CPPCHECK_FOUND cppcheck)
 if (CPPCHECK_FOUND)
@@ -110,6 +115,7 @@ SET(CMAKE_USE_RELATIVE_PATHS ON)
 
 add_subdirectory(net)
 add_subdirectory(cv-node)
+add_subdirectory(rep-server)
 
 ### Generate Build Configuration Files =========================================
 
diff --git a/cv-node/CMakeLists.txt b/cv-node/CMakeLists.txt
index cafa37799cef27a37f5b9358116ec8c8b7e9d390..926be4de8cd1ac4e0fd77ea7b5acb896d64161cc 100644
--- a/cv-node/CMakeLists.txt
+++ b/cv-node/CMakeLists.txt
@@ -4,11 +4,6 @@ include_directories(${PROJECT_SOURCE_DIR}/cv-node/include)
 
 add_subdirectory(lib)
 
-# Check for optional opencv components
-set(CMAKE_REQUIRED_INCLUDES ${OpenCV_INCLUDE_DIRS})
-check_include_file_cxx("opencv2/viz.hpp" HAVE_VIZ)
-check_include_file_cxx("opencv2/cudastereo.hpp" HAVE_OPENCVCUDA)
-
 set(CVNODESRC
 	../common/cpp/src/config.cpp
 	src/main.cpp
@@ -17,6 +12,7 @@ set(CVNODESRC
 	src/sync.cpp
 	src/display.cpp
 	src/disparity.cpp
+	src/streamer.cpp
 	src/middlebury.cpp
 	src/algorithms/rtcensus.cpp
 	src/algorithms/rtcensus_sgm.cpp
diff --git a/cv-node/config/config.json b/cv-node/config/config.json
index 1979288d6ae9921c0b25fa14a4de2be2f077cde3..104b18dc607ed2328c183e35292e3251e33e27e0 100644
--- a/cv-node/config/config.json
+++ b/cv-node/config/config.json
@@ -48,11 +48,25 @@
 		"lambda": 8000.0
 	},
 	"display": {
+		"flip_vert": false,
 		"disparity": false,
 		"points": true,
 		"depth": false,
 		"left": false,
 		"right": false
+	},
+	"net": {
+		"listen": "tcp://*:9001",
+		"peers": []
+	},
+	"stream": {
+		"name": "dummy"
+	},
+	"representation": {
+		"net": {
+			"peers": ["tcp://localhost:9001"]
+		},
+		"source": "ftl://utu.fi/dummy/rgb-d"
 	}
 }
 
diff --git a/cv-node/src/main.cpp b/cv-node/src/main.cpp
index ddc805e4d901a519ca0c72ab645004a60756264f..31e1c4100689d47ed0a7b0b64cca45336b2a915f 100644
--- a/cv-node/src/main.cpp
+++ b/cv-node/src/main.cpp
@@ -19,6 +19,8 @@
 #include <ftl/disparity.hpp>
 #include <ftl/middlebury.hpp>
 #include <ftl/display.hpp>
+#include <ftl/streamer.hpp>
+#include <ftl/net/universe.hpp>
 #include <nlohmann/json.hpp>
 
 #include "opencv2/imgproc.hpp"
@@ -33,8 +35,10 @@
 using ftl::Calibrate;
 using ftl::LocalSource;
 using ftl::Display;
+using ftl::Streamer;
 using ftl::Disparity;
 using ftl::SyncSource;
+using ftl::net::Universe;
 using std::string;
 using std::vector;
 using std::map;
@@ -114,7 +118,7 @@ static void process_options(const map<string, string> &opts) {
 }
 
 static void run(const string &file) {
-	// TODO(nick) Initiate the network
+	Universe net(config["net"]);
 
 	LocalSource *lsrc;
 	if (file != "") {
@@ -142,6 +146,7 @@ static void run(const string &file) {
 	Mat l, r, disp;
 
 	Display display(calibrate, config["display"]);
+	Streamer stream(net, config["stream"]);
 
 	while (display.active()) {
 		// Read calibrated images.
@@ -160,7 +165,7 @@ static void run(const string &file) {
 		// Send RGB+Depth images for local rendering
 		display.render(l, disp);
 
-		// streamer.send(l, disparity32F);
+		stream.send(l, disp);
 	}
 }
 
diff --git a/rep-server/CMakeLists.txt b/rep-server/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..164e2f2f429f53c69436960770497d186d02aa27
--- /dev/null
+++ b/rep-server/CMakeLists.txt
@@ -0,0 +1,17 @@
+# Need to include staged files and libs
+include_directories(${PROJECT_SOURCE_DIR}/rep-server/include)
+#include_directories(${PROJECT_BINARY_DIR})
+
+
+set(REPSRC
+	../common/cpp/src/config.cpp
+	src/main.cpp
+)
+
+add_executable(rep-server ${REPSRC})
+add_dependencies(rep-server ftlnet)
+
+#target_include_directories(cv-node PUBLIC ${PROJECT_SOURCE_DIR}/include)
+target_link_libraries(rep-server Threads::Threads ${OpenCV_LIBS} glog::glog ftlnet)
+
+
diff --git a/rep-server/src/main.cpp b/rep-server/src/main.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..b01fcf16b0cc01685fe151b11088432706b05a74
--- /dev/null
+++ b/rep-server/src/main.cpp
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2019 Nicolas Pope. All rights reserved.
+ *
+ * See LICENSE.
+ */
+
+#include <glog/logging.h>
+#include <ftl/config.h>
+
+#include <string>
+#include <map>
+#include <vector>
+#include <fstream>
+#include <thread>
+#include <chrono>
+#include <mutex>
+
+#include <opencv2/opencv.hpp>
+#include <ftl/net/universe.hpp>
+#include <nlohmann/json.hpp>
+
+#include "opencv2/imgproc.hpp"
+#include "opencv2/imgcodecs.hpp"
+#include "opencv2/highgui.hpp"
+#include "opencv2/core/utility.hpp"
+
+#ifdef WIN32
+#pragma comment(lib, "Rpcrt4.lib")
+#endif
+
+using ftl::net::Universe;
+using std::string;
+using std::vector;
+using std::map;
+using cv::Mat;
+using json = nlohmann::json;
+using std::ifstream;
+using std::this_thread::sleep_for;
+using std::chrono::milliseconds;
+using std::mutex;
+using std::unique_lock;
+
+// Store loaded configuration
+static json config;
+
+/**
+ * Find and load a JSON configuration file
+ */
+static bool findConfiguration(const string &file) {
+	ifstream i;
+	
+	if (file != "") i.open(file);
+	if (!i.is_open()) i.open("./config.json");
+	if (!i.is_open()) i.open(FTL_LOCAL_CONFIG_ROOT "/config.json");
+	if (!i.is_open()) i.open(FTL_GLOBAL_CONFIG_ROOT "/config.json");
+	if (!i.is_open()) return false;
+	i >> config;
+	config = config["representation"];
+	return true;
+}
+
+/**
+ * Generate a map from command line option to value
+ */
+map<string, string> read_options(char ***argv, int *argc) {
+	map<string, string> opts;
+
+	while (*argc > 0) {
+		string cmd((*argv)[0]);
+		if (cmd[0] != '-') break;
+
+		size_t p;
+		if ((p = cmd.find("=")) == string::npos) {
+			opts[cmd.substr(2)] = "true";
+		} else {
+			opts[cmd.substr(2, p-2)] = cmd.substr(p+1);
+		}
+
+		(*argc)--;
+		(*argv)++;
+	}
+
+	return opts;
+}
+
+/**
+ * Put command line options into json config. If config element does not exist
+ * or is of a different type then report an error.
+ */
+static void process_options(const map<string, string> &opts) {
+	for (auto opt : opts) {
+		if (opt.first == "config") continue;
+
+		if (opt.first == "version") {
+			std::cout << "FTL Vision Node - v" << FTL_VERSION << std::endl;
+			std::cout << FTL_VERSION_LONG << std::endl;
+			exit(0);
+		}
+
+		try {
+			auto ptr = json::json_pointer("/"+opt.first);
+			// TODO(nick) Allow strings without quotes
+			auto v = json::parse(opt.second);
+			if (v.type() != config.at(ptr).type()) {
+				LOG(ERROR) << "Incorrect type for argument " << opt.first;
+				continue;
+			}
+			config.at(ptr) = v;
+		} catch(...) {
+			LOG(ERROR) << "Unrecognised option: " << opt.first;
+		}
+	}
+}
+
+static void run(const string &file) {
+	Universe net(config["net"]);
+	Mat rgb;
+	mutex m;
+	
+	// Make sure connections are complete
+	sleep_for(milliseconds(500));
+
+	net.subscribe(config["source"], [&rgb,&m](const vector<unsigned char> &jpg) {
+		unique_lock<mutex> lk(m);
+		cv::imdecode(jpg, cv::IMREAD_COLOR, &rgb);
+		//LOG(INFO) << "Received JPG : " << rgb.cols;
+	});
+	
+	while (true) {
+		unique_lock<mutex> lk(m);
+		if (rgb.cols > 0) {
+			cv::imshow("RGB", rgb);
+		}
+		lk.unlock();
+		if (cv::waitKey(5) == 27) break;
+	}
+}
+
+int main(int argc, char **argv) {
+	argc--;
+	argv++;
+
+	// Process Arguments
+	auto options = read_options(&argv, &argc);
+	if (!findConfiguration(options["config"])) {
+		LOG(FATAL) << "Could not find any configuration!";
+	}
+	process_options(options);
+
+	run((argc > 0) ? argv[0] : "");
+}
+