diff --git a/components/structures/CMakeLists.txt b/components/structures/CMakeLists.txt
index fbbddc846b25e50493300c31a7ed57b87bc2bcbf..5df4a3870d0caccbef30d9d622dcf6268cf8e83d 100644
--- a/components/structures/CMakeLists.txt
+++ b/components/structures/CMakeLists.txt
@@ -1,10 +1,10 @@
 
-add_library(ftldata INTERFACE)
+add_library(ftldata STATIC ./src/new_frame.cpp)
 
-target_include_directories(ftldata INTERFACE
+target_include_directories(ftldata PUBLIC
 	${CMAKE_CURRENT_SOURCE_DIR}/include)
 
-target_link_libraries(ftldata INTERFACE ftlcommon Eigen3::Eigen ftlcodecs)
+target_link_libraries(ftldata ftlcommon Eigen3::Eigen ftlcodecs)
 
-#add_subdirectory(test)
+add_subdirectory(test)
 
diff --git a/components/structures/include/ftl/data/new_frame.hpp b/components/structures/include/ftl/data/new_frame.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..caec76a6d150e630a584b097426707cc67501a55
--- /dev/null
+++ b/components/structures/include/ftl/data/new_frame.hpp
@@ -0,0 +1,147 @@
+#ifndef _FTL_DATA_NEWFRAME_HPP_
+#define _FTL_DATA_NEWFRAME_HPP_
+
+#include <map>
+#include <unordered_set>
+#include <any>
+#include <optional>
+#include <list>
+#include <unordered_map>
+#include <ftl/codecs/channels.hpp>
+#include <ftl/exception.hpp>
+
+namespace ftl {
+namespace data {
+
+class Frame {
+	public:
+	uint32_t id=0;
+	int64_t timestamp=0;
+
+	public:
+	Frame() : parent_(nullptr) {};
+	explicit Frame(Frame *parent) : parent_(parent) {};
+	~Frame() { flush(); };
+
+	inline bool has(ftl::codecs::Channel c) {
+		return data_.find(c) != data_.end() || (parent_ && parent_->has(c));
+	}
+
+	inline bool changed(ftl::codecs::Channel c) {
+		return changed_.find(c) != changed_.end();
+	}
+
+	template <typename T>
+	bool isType(ftl::codecs::Channel c);
+
+	template <typename T>
+	const T *get(ftl::codecs::Channel c) const;
+
+	template <typename T>
+	T *getMutable(ftl::codecs::Channel c);
+
+	inline void touch(ftl::codecs::Channel c) {
+		changed_.emplace(c);
+	}
+
+	inline void untouch(ftl::codecs::Channel c) {
+		changed_.erase(c);
+	}
+
+	template <typename T>
+	void create(ftl::codecs::Channel c, const T &value);
+
+	template <typename T>
+	void create(ftl::codecs::Channel c);
+
+	template <typename T>
+	void push(ftl::codecs::Channel c, const T &v);
+
+	template <typename T>
+	void set(ftl::codecs::Channel c, const T &v);
+
+	inline void on(ftl::codecs::Channel c, const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+		if (parent_) parent_->on(c, cb);
+		else triggers_[c].push_back(cb);  // TODO: Find better way to enable removal
+	}
+
+	inline void on(const std::function<bool(Frame&,ftl::codecs::Channel)> &cb) {
+		any_triggers_.push_back(cb);
+	}
+
+	/** Send changes back through origin stream. */
+	bool flush();
+
+	// onBeginFlush
+	// onEndFlush
+	// onError
+
+	private:
+	std::map<ftl::codecs::Channel, std::any> data_;
+	std::unordered_set<ftl::codecs::Channel> changed_;
+	std::unordered_map<ftl::codecs::Channel, std::list<std::function<bool(Frame&,ftl::codecs::Channel)>>> triggers_;
+	std::list<std::function<bool(Frame&,ftl::codecs::Channel)>> any_triggers_;
+	Frame *parent_;
+};
+
+}
+}
+
+// ==== Implementations ========================================================
+
+template <typename T>
+bool ftl::data::Frame::isType(ftl::codecs::Channel c) {
+	auto i = data_.find(c);
+	if (i != data_.end()) {
+		return typeid(T) == i->second.type();
+	} else return false;
+}
+
+template <typename T>
+const T *ftl::data::Frame::get(ftl::codecs::Channel c) const {
+	auto i = data_.find(c);
+	if (i != data_.end()) {
+		return std::any_cast<T>(&i->second);
+	} else if (parent_) {
+		return parent_->get<T>(c);
+	} else return nullptr;
+}
+
+template <typename T>
+void ftl::data::Frame::create(ftl::codecs::Channel c, const T &value) {
+	data_[c] = value;
+	touch(c);
+}
+
+template <typename T>
+void ftl::data::Frame::create(ftl::codecs::Channel c) {
+	if (!isType<T>(c)) data_[c] = T{};
+	touch(c);
+}
+
+template <typename T>
+void ftl::data::Frame::push(ftl::codecs::Channel c, const T &v) {
+	auto i = data_.find(c);
+	if (i != data_.end()) {
+		auto *p = std::any_cast<std::vector<T>>(&i->second);
+		p->push_back(v);
+	} else {
+		throw FTL_Error("Push on missing channel (" << static_cast<unsigned int>(c) << ")");
+	}
+	touch(c);
+}
+
+template <typename T>
+void ftl::data::Frame::set(ftl::codecs::Channel c, const T &v) {
+	auto i = data_.find(c);
+	if (i != data_.end()) {
+		i->second = v;
+	} else if (parent_ && parent_->isType<T>(c)) {
+		create<T>(c, v);
+	} else {
+		throw FTL_Error("Set on missing channel (" << static_cast<unsigned int>(c) << ")");
+	}
+	touch(c);
+}
+
+#endif
\ No newline at end of file
diff --git a/components/structures/src/new_frame.cpp b/components/structures/src/new_frame.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..52f40ad7e21600050da6134141e0e899d0184697
--- /dev/null
+++ b/components/structures/src/new_frame.cpp
@@ -0,0 +1,32 @@
+#include <ftl/data/new_frame.hpp>
+
+using ftl::data::Frame;
+
+#define LOGURU_REPLACE_GLOG 1
+#include <loguru.hpp>
+
+bool Frame::flush() {
+	if (parent_) {
+		for (auto c : changed_) {
+			parent_->changed_.emplace(c);
+			parent_->data_[c] = std::move(data_[c]);
+			data_.erase(c);
+		}
+		parent_->flush();
+	} else {
+		for (auto c : changed_) {
+			auto i = triggers_.find(c);
+			if (i != triggers_.end()) {
+				for (auto f : i->second) {
+					try {
+						f(*this, c);
+					} catch (const std::exception &e) {
+						LOG(ERROR) << "Exception in frame flush: " << e.what();
+					}
+				}
+			}
+		}
+	}
+	changed_.clear();
+	return true;
+}
diff --git a/components/structures/test/CMakeLists.txt b/components/structures/test/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..155634bc4eb4fda07ad55405d1ceabd7d5293aa6
--- /dev/null
+++ b/components/structures/test/CMakeLists.txt
@@ -0,0 +1,9 @@
+### Frame Unit #################################################################
+add_executable(nframe_unit
+	$<TARGET_OBJECTS:CatchTest>
+	./frame_unit.cpp
+	../src/new_frame.cpp
+)
+target_include_directories(nframe_unit PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/../include")
+target_link_libraries(nframe_unit
+	ftlcommon ftlcodecs)
\ No newline at end of file
diff --git a/components/structures/test/frame_unit.cpp b/components/structures/test/frame_unit.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..a866b8e440228f84ec060999995ba0db09d05244
--- /dev/null
+++ b/components/structures/test/frame_unit.cpp
@@ -0,0 +1,243 @@
+#include "catch.hpp"
+
+#include <ftl/data/new_frame.hpp>
+
+
+using ftl::data::Frame;
+using ftl::codecs::Channel;
+
+TEST_CASE("ftl::data::Frame create get", "[Frame]") {
+	SECTION("write and read integers") {
+		Frame f;
+		f.create<int>(Channel::Pose, 55);
+
+		auto x = f.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 55 );
+	}
+
+	SECTION("write and read floats") {
+		Frame f;
+		f.create<float>(Channel::Pose, 44.0f);
+
+		auto x = f.get<float>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 44.0f );
+	}
+
+	SECTION("write and read structures") {
+		struct Test {
+			int a=44;
+			float b=33.0f;
+		};
+		Frame f;
+		f.create<Test>(Channel::Pose, {});
+
+		auto x = f.get<Test>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( x->a == 44 );
+		REQUIRE( x->b == 33.0f );
+	}
+
+	SECTION("write and read fail") {
+		struct Test {
+			int a=44;
+			float b=33.0f;
+		};
+		Frame f;
+		f.create<Test>(Channel::Pose, {});
+
+		auto x = f.get<int>(Channel::Pose);
+		REQUIRE( !x );
+	}
+}
+
+TEST_CASE("ftl::data::Frame isType", "[Frame]") {
+	SECTION("is int type") {
+		Frame f;
+		f.create<int>(Channel::Pose, 55);
+
+		REQUIRE( f.isType<int>(Channel::Pose) );
+		REQUIRE( !f.isType<float>(Channel::Pose) );
+	}
+
+	SECTION("is struct type") {
+		struct Test {
+			int a; int b;
+		};
+
+		Frame f;
+		f.create<Test>(Channel::Pose, {3,4});
+
+		REQUIRE( f.isType<Test>(Channel::Pose) );
+		REQUIRE( !f.isType<float>(Channel::Pose) );
+	}
+
+	SECTION("missing") {
+		Frame f;
+
+		REQUIRE( !f.isType<float>(Channel::Pose) );
+	}
+}
+
+TEST_CASE("ftl::data::Frame changed", "[Frame]") {
+	SECTION("change on create") {
+		Frame f;
+
+		REQUIRE( !f.changed(Channel::Pose) );
+		f.create<int>(Channel::Pose, 55);
+		REQUIRE( f.changed(Channel::Pose) );
+	}
+
+	SECTION("no change on untouch") {
+		Frame f;
+
+		f.create<int>(Channel::Pose, 55);
+		REQUIRE( f.changed(Channel::Pose) );
+		f.untouch(Channel::Pose);
+		REQUIRE( !f.changed(Channel::Pose) );
+	}
+}
+
+TEST_CASE("ftl::data::Frame create", "[Frame]") {
+	SECTION("same value on create") {
+		Frame f;
+
+		f.create<int>(Channel::Pose, 55);
+		auto x = f.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 55 );
+
+		f.create<int>(Channel::Pose);
+		auto y = f.get<int>(Channel::Pose);
+		REQUIRE( y );
+		REQUIRE( *y == 55 );
+
+		REQUIRE( x == y );
+	}
+
+	SECTION("change of type") {
+		Frame f;
+
+		f.create<int>(Channel::Pose, 55);
+		auto x = f.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 55 );
+
+		f.create<float>(Channel::Pose);
+		auto y = f.get<float>(Channel::Pose);
+		REQUIRE( y );
+		REQUIRE( *y == 0.0f );
+	}
+}
+
+TEST_CASE("ftl::data::Frame use of parent", "[Frame]") {
+	SECTION("get from parent") {
+		Frame p;
+		Frame f(&p);
+
+		p.create<int>(Channel::Pose, 55);
+
+		auto x = f.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 55 );
+
+		auto y = p.get<int>(Channel::Pose);
+		REQUIRE( x == y );
+	}
+
+	SECTION("has from parent") {
+		Frame p;
+		Frame f(&p);
+
+		p.create<int>(Channel::Pose, 55);
+		REQUIRE( f.has(Channel::Pose) );
+	}
+
+	SECTION("no change in parent") {
+		Frame p;
+		Frame f(&p);
+
+		p.create<int>(Channel::Pose, 55);
+		p.untouch(Channel::Pose);
+
+		REQUIRE( !f.changed(Channel::Pose) );
+		REQUIRE( !p.changed(Channel::Pose) );
+
+		f.set<int>(Channel::Pose, 66);
+
+		REQUIRE( f.changed(Channel::Pose) );
+		REQUIRE( !p.changed(Channel::Pose) );
+
+		auto x = f.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 66 );
+
+		auto y = p.get<int>(Channel::Pose);
+		REQUIRE( y );
+		REQUIRE( *y == 55 );
+	}
+}
+
+TEST_CASE("ftl::data::Frame flush", "[Frame]") {
+	SECTION("event on flush") {
+		Frame f;
+
+		int event = 0;
+		f.on(Channel::Pose, [&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<int>(Channel::Pose, 55);
+		REQUIRE( event == 0 );
+
+		f.flush();
+		REQUIRE( event == 1 );
+	}
+
+	SECTION("parent event on flush") {
+		Frame p;
+		Frame f(&p);
+
+		int event = 0;
+		p.on(Channel::Pose, [&event](Frame &frame, Channel c) {
+			event++;
+			return true;
+		});
+
+		f.create<int>(Channel::Pose, 55);
+		REQUIRE( event == 0 );
+
+		f.flush();
+		REQUIRE( event == 1 );
+	}
+
+	SECTION("parent change on flush") {
+		Frame p;
+		Frame f(&p);
+
+		p.create<int>(Channel::Pose, 55);
+		p.flush();
+
+		f.set<int>(Channel::Pose, 66);
+		auto x = p.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 55 );
+		
+		f.flush();
+		x = p.get<int>(Channel::Pose);
+		REQUIRE( x );
+		REQUIRE( *x == 66 );
+	}
+
+	SECTION("untouched on flush") {
+		Frame f;
+
+		f.create<int>(Channel::Pose, 55);
+		REQUIRE( f.changed(Channel::Pose) );
+
+		f.flush();
+		REQUIRE( !f.changed(Channel::Pose) );
+	}
+}