diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..94143827ed065ca0d7d5be1b765d255c5c32cd9a
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+Dockerfile
diff --git a/.gitignore b/.gitignore
index 7f7adc5d269adf15ba7ae4e4de2930ae05ca0a02..3256d9fa27698c2d00976e1f05fc9a35060e9179 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /build
-/.vscode
\ No newline at end of file
+/.vscode
+__pycache__
\ No newline at end of file
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 683d5b4df68a7a3502f7bfdb252447ea07c01da0..a0ecd16e19b20f6ac993b5a0fe05c2ac63ed8a3f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,22 +1,36 @@
-stages:
-  - build
-  - run
+# include:
+#     - project: 'vr-group/in-situ-pipeline/insite'
+#       ref: develop
+#       file: '/test-setup.yml'
 
-docker:build:
-  stage: build
-  tags:
-    - centos
-    - docker
-  script:
-    - docker build -t insite-nest-module .
+# variables:
+#     INSITE_NEST_MODULE_COMMIT: $CI_COMMIT_SHA
 
-docker:run:
-  stage: run
-  tags:
-    - centos
-    - docker
-  dependencies:
-    - docker:build
-  script:
-    - docker run --publish 8000:8000 insite-nest-module 20
+# api_test:
+#     extends: .api_test
 
+deploy:develop:
+    tags:
+        - docker
+        - centos
+    only:
+        - develop
+    script:
+        - docker build -t rwthvr/insite-nest-module:develop .
+        - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
+        - docker push rwthvr/insite-nest-module:develop
+
+deploy:master:
+    tags:
+        - docker
+        - centos
+    only:
+        - master
+    script:
+        - docker build -t rwthvr/insite-nest-module .
+        - VERSION=$(git tag --points-at $CI_COMMIT_SHA)
+        - >
+            [[ $VERSION =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]
+        - docker tag rwthvr/insite-nest-module rwthvr/insite-nest-module:$VERSION
+        - docker login -u $DOCKER_USERNAME -p $DOCKER_PASSWORD
+        - docker push rwthvr/insite-nest-module
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9f14d35c8b473383e7e56b098f2341d411658c85..3ee9ac482e30cca4d307d9c021d11224af35d421 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -105,6 +105,7 @@ project( ${MODULE_NAME} CXX )
 
 # Add cpprestsdk
 find_package(cpprestsdk REQUIRED)
+find_package(libpqxx REQUIRED)
 
 # Get the Python executable (for help generation).
 execute_process(
@@ -283,7 +284,7 @@ if ( BUILD_SHARED_LIBS )
   install( TARGETS ${MODULE_NAME}_module
       DESTINATION ${CMAKE_INSTALL_LIBDIR}
       )
-  target_link_libraries(${MODULE_NAME}_module PRIVATE cpprestsdk::cpprest)
+  target_link_libraries(${MODULE_NAME}_module PRIVATE cpprestsdk::cpprest libpqxx::pqxx_shared)
 endif ()
 
 # Build dynamic/static library for standard linking from NEST.
@@ -298,6 +299,7 @@ set_target_properties( ${MODULE_NAME}_lib
     COMPILE_FLAGS "${NEST_CXXFLAGS}"
     LINK_FLAGS "${NEST_LIBS}"
     OUTPUT_NAME ${MODULE_NAME} )
+target_link_libraries(${MODULE_NAME}_lib PRIVATE cpprestsdk::cpprest libpqxx::pqxx_shared)
 
 # Install library, header and sli init files.
 install( TARGETS ${MODULE_NAME}_lib DESTINATION ${CMAKE_INSTALL_LIBDIR} )
@@ -340,6 +342,11 @@ if ( ( NOT CMAKE_CROSSCOMPILING )
 
 endif ()
 
+configure_file(
+  ${CMAKE_SOURCE_DIR}/examples/run_brunel_simulation.sh.in
+  ${CMAKE_BINARY_DIR}/run_brunel_simulation.sh
+  @ONLY
+)
 
 message( "" )
 message( "-------------------------------------------------------" )
diff --git a/Dockerfile b/Dockerfile
index 56ec745c03264cd7d86c9a5b2511ade2189a490d..a8db0618eca26ac302a71505a18f1ed1134c47c6 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,26 +2,44 @@ FROM ubuntu:latest
 ARG DEBIAN_FRONTEND=noninteractive
 RUN apt-get update && apt-get install -y \
     cmake g++ make ninja-build python3 python3-dev python3-pip python3-numpy python3-scipy python3-matplotlib \
-    git gsl-bin libgsl0-dev libltdl-dev libtool \
+    git gsl-bin libgsl0-dev libltdl-dev libtool netcat \
     libboost-atomic-dev libboost-thread-dev libboost-system-dev libboost-date-time-dev libboost-regex-dev \
     libboost-filesystem-dev libboost-random-dev libboost-chrono-dev libboost-serialization-dev \
-    libwebsocketpp-dev openssl libssl-dev ninja-build
+    libwebsocketpp-dev openssl libssl-dev ninja-build \
+    openmpi-bin libopenmpi-dev libpq-dev postgresql-server-dev-all
 RUN pip3 install Cython
-RUN git clone --single-branch --branch nestio https://github.com/jougs/nest-simulator.git nest
+
+RUN git clone --single-branch --branch master https://github.com/nest/nest-simulator.git nest && \
+    cd nest && \
+    git checkout 5c0f41230dda9e4b99b8df89729ea43b340246ad && \
+    cd /
 WORKDIR /nest-build
 RUN cmake \
     -G Ninja \
+    -Dwith-mpi=ON \
     -DCMAKE_INSTALL_PREFIX=/nest-install \
     -DCMAKE_BUILD_TYPE=Release \
     /nest
 RUN ninja && ninja install
+
 RUN git clone --single-branch --branch v2.10.14 --recurse-submodules https://github.com/microsoft/cpprestsdk.git /cpprestsdk
 WORKDIR /cpprestsdk-build
 RUN cmake \
     -G Ninja \
     -DCMAKE_BUILD_TYPE=Release \
+    -DBUILD_TESTS=OFF \
     /cpprestsdk
 RUN ninja && ninja install
+
+RUN git clone --single-branch --branch 6.4.6 https://github.com/jtv/libpqxx.git /libpqxx
+WORKDIR /libpqxx-build
+RUN cmake \
+    -G Ninja \
+    -DCMAKE_BUILD_TYPE=Release \
+    -DBUILD_TESTS=OFF \
+    /libpqxx
+RUN ninja && ninja install
+
 COPY . /insite
 WORKDIR /insite-build
 RUN cmake \
@@ -30,4 +48,8 @@ RUN cmake \
     -DCMAKE_BUILD_TYPE=Release \
     /insite
 RUN ninja && ninja install
-ENTRYPOINT [ "/insite/examples/run.sh" ]
\ No newline at end of file
+ENV PGPASSWORD=postgres
+
+EXPOSE 8000
+ENTRYPOINT "/insite-build/run_brunel_simulation.sh"
+CMD 1000 2500 2
\ No newline at end of file
diff --git a/data_storage.cpp b/data_storage.cpp
index a6d4ddb65200d3cf4dfb95ab45cb9c5b3089453d..4ed7af7cdcedea4ff215bdef2f069af059474bb9 100644
--- a/data_storage.cpp
+++ b/data_storage.cpp
@@ -1,6 +1,7 @@
 #include "data_storage.hpp"
 
 #include <algorithm>
+#include <cstring>
 
 namespace insite {
 
@@ -37,21 +38,40 @@ DataStorage::DataStorage(
   // spikes_neurons_dataset_ =
   //     h5_file_->createDataSet("spikes/neurons", H5::PredType::NATIVE_UINT64,
   //                             spikes_data_space, spikes_set_properties_);
+  buffered_spikes_.reserve(32 * 1024 * 1024 / sizeof(Spike)); // Reserve 32mb
+  SetCurrentSimulationTime(0.0);
 }
 
-void DataStorage::AddSpike(std::uint64_t simulation_step, std::uint64_t gid) {
+void DataStorage::AddNeuronId(uint64_t neuron_id) {
+  std::unique_lock<std::mutex> lock(neuron_ids_mutex_);
+  const auto insert_position =
+      std::lower_bound(neuron_ids_.begin(), neuron_ids_.end(), neuron_id);
+  if (insert_position == neuron_ids_.end() || *insert_position != neuron_id) {
+    neuron_ids_.insert(insert_position, neuron_id);
+  }
+}
+
+std::vector<uint64_t> DataStorage::GetNeuronIds() {
+  std::unique_lock<std::mutex> lock(neuron_ids_mutex_);
+  std::vector<uint64_t> temp_neuron_ids = neuron_ids_;
+  return temp_neuron_ids;
+}
+
+void DataStorage::AddSpike(double simulation_time, std::uint64_t gid) {
   std::unique_lock<std::mutex> lock(spike_mutex_);
   constexpr auto spike_occured_before = [](const Spike& lhs, const Spike& rhs) {
-    return lhs.simulation_step < rhs.simulation_step;
+    return lhs.simulation_time < rhs.simulation_time;
   };
-  const Spike spike {simulation_step, gid};
-  const auto equal_range = std::equal_range(buffered_spikes_.begin(), buffered_spikes_.end(), spike, spike_occured_before);
+  const Spike spike{simulation_time, gid};
+  const auto equal_range =
+      std::equal_range(buffered_spikes_.begin(), buffered_spikes_.end(), spike,
+                       spike_occured_before);
   for (auto i = equal_range.first; i != equal_range.second; ++i) {
     if (i->gid == gid) {
       return;
     }
   }
-  buffered_spikes_.insert(equal_range.second,spike);
+  buffered_spikes_.insert(equal_range.second, spike);
 }
 
 std::vector<Spike> DataStorage::GetSpikes() {
@@ -91,4 +111,71 @@ void DataStorage::Flush() {
   // buffered_spikes_.clear();
 }
 
-}  // namespace insite
\ No newline at end of file
+void DataStorage::AddMultimeterMeasurement(std::uint64_t device_id,
+                                           const std::string& attribute_name,
+                                           const double simulation_time,
+                                           const std::uint64_t gid,
+                                           const double value) {
+  std::unique_lock<std::mutex> lock(measurement_mutex_);
+  auto& measurement = buffered_measurements_[device_id][attribute_name];
+  auto& simulation_times = measurement.simulation_times;
+  auto& gids = measurement.gids;
+  auto& values = measurement.values;
+
+  auto time_iterator = std::lower_bound(
+      simulation_times.begin(), simulation_times.end(), simulation_time);
+  auto time_index = std::distance(simulation_times.begin(), time_iterator);
+  if (time_iterator == simulation_times.end() ||
+      *time_iterator != simulation_time) {
+    simulation_times.insert(time_iterator, simulation_time);
+
+    auto new_values =
+        std::vector<double>(simulation_times.size() * gids.size(), 0.0);
+    for (std::size_t t = 0; t < simulation_times.size(); ++t)
+      for (std::size_t g = 0; g < gids.size(); ++g)
+        if (t != time_index)
+          new_values[t * gids.size() + g] =
+              values[(t > time_index ? t - 1 : t) * gids.size() + g];
+    values = new_values;
+  }
+
+  auto gid_iterator = std::lower_bound(gids.begin(), gids.end(), gid);
+  auto gid_index = std::distance(gids.begin(), gid_iterator);
+  if (gid_iterator == gids.end() || *gid_iterator != gid) {
+    gids.insert(gid_iterator, gid);
+
+    auto new_values =
+        std::vector<double>(simulation_times.size() * gids.size(), 0.0);
+    for (std::size_t t = 0; t < simulation_times.size(); ++t)
+      for (std::size_t g = 0; g < gids.size(); ++g)
+        if (g != gid_index)
+          new_values[t * gids.size() + g] =
+              values[t * gids.size() + (g > gid_index ? g - 1 : g)];
+    values = new_values;
+  }
+
+  values[time_index * gids.size() + gid_index] = value;
+}
+
+std::unordered_map<std::uint64_t,
+                   std::unordered_map<std::string, MultimeterMeasurements>>
+DataStorage::GetMultimeterMeasurements() {
+  std::unique_lock<std::mutex> lock(measurement_mutex_);
+  auto measurements = buffered_measurements_;
+  return measurements;
+}
+
+void DataStorage::SetCurrentSimulationTime(double simulation_time) {
+  uint64_t simulation_time_int;
+  memcpy(&simulation_time_int, &simulation_time, sizeof(simulation_time_int));
+  current_simulation_time_ = simulation_time_int;
+}
+
+double DataStorage::GetCurrentSimulationTime() const {
+  const uint64_t simulation_time_int =current_simulation_time_;
+  double simulation_time;
+  memcpy(&simulation_time, &simulation_time_int, sizeof(simulation_time));
+  return simulation_time;
+}
+
+}  // namespace insite
diff --git a/data_storage.hpp b/data_storage.hpp
index 89b27aef7c0d6fde243b114e13ee8911bd8a81b0..580a0c9348982197007202626aed2075394689f7 100644
--- a/data_storage.hpp
+++ b/data_storage.hpp
@@ -3,35 +3,74 @@
 
 #include <cstdint>
 #include <memory>
+#include <mutex>
 #include <string>
+#include <unordered_map>
 #include <vector>
-#include <mutex>
+#include <atomic>
 
 namespace insite {
 
 struct Spike {
-  std::uint64_t simulation_step;
+  double simulation_time;
   std::uint64_t gid;
 };
 static_assert(sizeof(Spike) == 2 * 8);
 
+struct MultimeterInfo {
+  std::uint64_t device_id;
+  bool needs_update;
+  std::vector<std::string> double_attributes;
+  std::vector<std::string> long_attributes;
+  std::vector<std::uint64_t> gids;
+};
+
+struct MultimeterMeasurements {
+  std::vector<double> simulation_times;
+  std::vector<std::uint64_t> gids;
+  std::vector<double> values;
+};
+
 class DataStorage {
  public:
   DataStorage(const std::string& filename
               /*, hsize_t time_chunk_size, hsize_t neuronids_chunk_size*/);
 
-  void AddSpike(std::uint64_t simulation_step, std::uint64_t gid);
+  void AddNeuronId(uint64_t neuron_ids);
+  std::vector<uint64_t> GetNeuronIds();
+
+  void AddSpike(double simulation_time, std::uint64_t gid);
   std::vector<Spike> GetSpikes();
   void Flush();
 
+  void AddMultimeterMeasurement(std::uint64_t device_id, 
+    const std::string& attribute_name, const double simulation_time,
+    const std::uint64_t gid, const double value);
+  std::unordered_map<std::uint64_t, std::unordered_map<std::string, 
+    MultimeterMeasurements>> GetMultimeterMeasurements();
+
+  void SetCurrentSimulationTime(double simulation_time);
+  double GetCurrentSimulationTime() const;
+
  private:
   // std::unique_ptr<H5::H5File> h5_file_;
 
+  std::mutex neuron_ids_mutex_;
+  std::vector<uint64_t> neuron_ids_;
+
   // uint64_t flushed_spikes_count = 0;
   std::vector<Spike> buffered_spikes_;
   // H5::DataSet spikes_times_dataset_;
   // H5::DataSet spikes_neurons_dataset_;
   std::mutex spike_mutex_;
+
+  std::atomic_uint64_t current_simulation_time_;
+
+  // Device ID to attribute index to measurement map.
+  std::unordered_map<std::uint64_t, std::unordered_map<std::string, 
+    MultimeterMeasurements>> buffered_measurements_;
+
+  std::mutex measurement_mutex_;
 };
 
 }  // namespace insite
diff --git a/examples/sim2.py b/examples/brunel_simulation.py
similarity index 87%
rename from examples/sim2.py
rename to examples/brunel_simulation.py
index 5e72710f5627d8e167aed6df22fe89528717d2a4..6dc936c0ed5798313acec20fad053c3274c315ca 100644
--- a/examples/sim2.py
+++ b/examples/brunel_simulation.py
@@ -44,12 +44,21 @@ References
 # Import all necessary modules for simulation, analysis and plotting.
 
 import nest
-import nest.raster_plot
+import signal
+import math
 
 import time
 import sys
 from numpy import exp
 
+
+def sigint_handler(sig, frame):
+    print('Exiting...')
+    sys.exit(0)
+
+
+signal.signal(signal.SIGINT, sigint_handler)
+
 nest.Install("insitemodule")
 
 nest.ResetKernel()
@@ -81,11 +90,11 @@ epsilon = 0.1  # connection probability
 # Definition of the number of neurons in the network and the number of neuron
 # recorded from
 
-order = 2500
+order = int(sys.argv[2]) if len(
+    sys.argv) > 2 else 2500 # Should be square, otherwise the position grid becomes invalid
 NE = 4 * order  # number of excitatory neurons
 NI = 1 * order  # number of inhibitory neurons
 N_neurons = NE + NI  # number of neurons in total
-N_rec = 4  # record from 50 neurons
 
 ###############################################################################
 # Definition of connectivity parameter
@@ -127,7 +136,7 @@ p_rate = 1000.0 * nu_ex * CE
 # already processed simulation time as well as its percentage of the total
 # simulation time.
 
-nest.SetKernelStatus({"resolution": dt, "print_time": True,
+nest.SetKernelStatus({"resolution": dt,
                       "overwrite_files": True})
 
 print("Building network")
@@ -148,21 +157,11 @@ nest.SetDefaults("poisson_generator", {"rate": p_rate})
 # as the poisson generator and two spike detectors. The spike detectors will
 # later be used to record excitatory and inhibitory spikes.
 
-nodes_ex = nest.Create("iaf_psc_delta", NE)
-nodes_in = nest.Create("iaf_psc_delta", NI)
+nodes_ex = nest.Create("iaf_psc_delta", positions=nest.spatial.grid([int(math.sqrt(NE)), int(math.sqrt(NE))]))
+nodes_in = nest.Create("iaf_psc_delta", positions=nest.spatial.grid([int(math.sqrt(NI)), int(math.sqrt(NI))]))
 noise = nest.Create("poisson_generator")
 espikes = nest.Create("spike_detector")
 ispikes = nest.Create("spike_detector")
-multimeter = nest.Create("multimeter")
-multimeter2 = nest.Create("multimeter")
-ascii_multimeter = nest.Create("multimeter")
-
-###############################################################################
-# Configuration of the spike detectors recording excitatory and inhibitory
-# spikes using ``SetStatus``, which expects a list of node handles and a list
-# of parameter dictionaries. Setting the property `record_to` to *"ascii"*
-# ensures that the spikes will be recorded to a file, whose name starts with
-# the string assigned to label.
 
 nest.SetStatus(espikes, [{"label": "brunel-py-ex",
                           "record_to": "insite"}])
@@ -170,11 +169,6 @@ nest.SetStatus(espikes, [{"label": "brunel-py-ex",
 nest.SetStatus(ispikes, [{"label": "brunel-py-in",
                           "record_to": "insite"}])
 
-nest.SetStatus(multimeter, {"record_from": ["V_m"], "record_to": "insite", })
-nest.SetStatus(multimeter2, {"record_from": ["V_m"], "record_to": "insite", })
-nest.SetStatus(ascii_multimeter, {"record_from": [
-               "V_m"], "record_to": "ascii", })
-
 print("Connecting devices")
 
 ###############################################################################
@@ -207,12 +201,8 @@ nest.Connect(noise, nodes_in, syn_spec="excitatory")
 # Here the same shortcut for the specification of the synapse as defined
 # above is used.
 
-nest.Connect(nodes_ex[:N_rec], espikes, syn_spec="excitatory")
-nest.Connect(nodes_in[:N_rec], ispikes, syn_spec="excitatory")
-nest.Connect(multimeter, nodes_ex[:N_rec], syn_spec="excitatory")
-nest.Connect(multimeter, nodes_in[:N_rec], syn_spec="excitatory")
-nest.Connect(ascii_multimeter, nodes_ex[:N_rec], syn_spec="excitatory")
-nest.Connect(ascii_multimeter, nodes_in[:N_rec], syn_spec="excitatory")
+nest.Connect(nodes_ex, espikes, syn_spec="excitatory")
+nest.Connect(nodes_in, ispikes, syn_spec="excitatory")
 
 print("Connecting network")
 
@@ -227,7 +217,8 @@ print("Excitatory connections")
 # suffices to insert a string.
 
 conn_params_ex = {'rule': 'fixed_indegree', 'indegree': CE}
-nest.Connect(nodes_ex, nodes_ex + nodes_in, conn_params_ex, "excitatory")
+nest.Connect(nodes_ex, nodes_ex, conn_params_ex, "excitatory")
+nest.Connect(nodes_ex, nodes_in, conn_params_ex, "excitatory")
 
 print("Inhibitory connections")
 
@@ -238,7 +229,8 @@ print("Inhibitory connections")
 # population defined above.
 
 conn_params_in = {'rule': 'fixed_indegree', 'indegree': CI}
-nest.Connect(nodes_in, nodes_ex + nodes_in, conn_params_in, "inhibitory")
+nest.Connect(nodes_in, nodes_ex, conn_params_in, "inhibitory")
+nest.Connect(nodes_in, nodes_in, conn_params_in, "inhibitory")
 
 ###############################################################################
 # Storage of the time point after the buildup of the network in a variable.
@@ -270,8 +262,8 @@ events_in = nest.GetStatus(ispikes, "n_events")[0]
 # neurons recorded from and the simulation time. The multiplication by 1000.0
 # converts the unit 1/ms to 1/s=Hz.
 
-rate_ex = events_ex / simtime * 1000.0 / N_rec
-rate_in = events_in / simtime * 1000.0 / N_rec
+rate_ex = events_ex / simtime * 1000.0 / NE
+rate_in = events_in / simtime * 1000.0 / NI
 
 ###############################################################################
 # Reading out the number of connections established using the excitatory and
@@ -300,3 +292,10 @@ print("Excitatory rate   : %.2f Hz" % rate_ex)
 print("Inhibitory rate   : %.2f Hz" % rate_in)
 print("Building time     : %.2f s" % build_time)
 print("Simulation time   : %.2f s" % sim_time)
+
+try:
+    input("Press Enter to quit...")
+except EOFError:
+    print("Simulation finished, press ctrl+c to exit.")
+    while True:
+        time.sleep(1)
diff --git a/examples/run.sh b/examples/run.sh
deleted file mode 100755
index c730a500da1046cd24bdaaec2e901e3f703e7b2b..0000000000000000000000000000000000000000
--- a/examples/run.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/bash
-source /nest-install/bin/nest_vars.sh
-export LD_LIBRARY_PATH=$NEST_MODULE_PATH:/usr/local/lib/:$LD_LIBRARY_PATH
-python3 /insite/examples/sim2.py $1
\ No newline at end of file
diff --git a/examples/run_brunel_simulation.sh.in b/examples/run_brunel_simulation.sh.in
new file mode 100755
index 0000000000000000000000000000000000000000..be009df14365664f74531417bc418b9698628679
--- /dev/null
+++ b/examples/run_brunel_simulation.sh.in
@@ -0,0 +1,5 @@
+#!/bin/bash
+source @NEST_INSTALL_PREFIX@/bin/nest_vars.sh
+export LD_LIBRARY_PATH=$NEST_MODULE_PATH:/usr/local/lib/:$LD_LIBRARY_PATH
+# python3 @CMAKE_SOURCE_DIR@/examples/brunel_simulation.py $1 $2 # Uncomment this to run normally
+time mpirun -n $3 --mca btl_vader_single_copy_mechanism none --allow-run-as-root -x PYTHONPATH python3 @CMAKE_SOURCE_DIR@/examples/brunel_simulation.py $1 $2
\ No newline at end of file
diff --git a/examples/simulation.py b/examples/simulation.py
deleted file mode 100644
index 6d770c92d570bbd95d9d3af8ae0bfb16bc4ab272..0000000000000000000000000000000000000000
--- a/examples/simulation.py
+++ /dev/null
@@ -1,80 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# soundclick_example.py
-#
-# This file is part of NEST.
-#
-# Copyright (C) 2004 The NEST Initiative
-#
-# NEST is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 2 of the License, or
-# (at your option) any later version.
-#
-# NEST is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with NEST.  If not, see <http://www.gnu.org/licenses/>.
-
-'''
-soundclick recording backend example
-------------------------------------
-
-Example PyNest script to demonstrate the soundclick recording backend.
-
-Recorded spike events produce a clicking sound similar to that heard in
-electrophysiological recordings. The recording backend makes use of the
-SFML (Simple and Fast Multimedia Library) audio module.
-This requires libsfml-dev to be installed. To create the illusion of a
-realistic sound from an electrophysiological recording, the recording
-backend slows down the simulation to biological real time.
-'''
-
-import nest
-nest.Install("insitemodule")
-
-population = nest.Create("izhikevich", 100)
-multimeter = nest.Create("multimeter", 1)
-ascii_multimeter = nest.Create("multimeter", 1)
-nest.SetStatus(multimeter, {"record_from": ["V_m"], "record_to": "insite", })
-nest.SetStatus(ascii_multimeter, {"record_from": [
-               "V_m"], "record_to": "ascii", })
-nest.Connect(multimeter, population)
-nest.Connect(ascii_multimeter, population)
-
-# regular spiking
-nest.SetStatus(population, {"a": 0.02,
-                            "b": 0.2,
-                            "c": -65.0,
-                            "d": 8.0,
-                            "U_m": 0.0,
-                            "V_m": -75.0,
-                            "I_e": 6.0})
-nest.Simulate(4000)
-nest.SetStatus(population, {"I_e": 0.0})
-nest.Simulate(500)
-
-# fast spiking
-nest.SetStatus(population, {"a": 0.1,
-                            "b": 0.2,
-                            "c": -65.0,
-                            "d": 2.0,
-                            "U_m": 0.0,
-                            "V_m": -75.0,
-                            "I_e": 6.0})
-nest.Simulate(4000)
-nest.SetStatus(population, {"I_e": 0.0})
-nest.Simulate(500)
-
-# chattering
-nest.SetStatus(population, {"a": 0.02,
-                            "b": 0.2,
-                            "c": -50.0,
-                            "d": 2.0,
-                            "U_m": 0.0,
-                            "V_m": -75.0,
-                            "I_e": 6.0})
-nest.Simulate(4000)
diff --git a/http_server.cpp b/http_server.cpp
index e5b37fb5185459734b01e18c62ecdf408800824e..e2d2d9c3d16511c5fece8fe4cb0e809ab98c6b58 100644
--- a/http_server.cpp
+++ b/http_server.cpp
@@ -1,14 +1,29 @@
 #include "http_server.hpp"
+
+#include <algorithm>
+#include <iostream>
+#include <regex>
+#include <unordered_set>
+
 #include "data_storage.hpp"
 
 namespace insite {
 
-HttpServer::HttpServer(web::http::uri address, DataStorage* storage)
-    : http_listener_{address}, storage_(storage) {
+HttpServer::HttpServer(web::http::uri address, DataStorage* storage,
+                       std::string database_uri)
+    : http_listener_{address},
+      storage_(storage),
+      database_uri_(database_uri) {
   http_listener_.support([this](web::http::http_request request) {
     if (request.method() == "GET" &&
         request.relative_uri().path() == "/spikes") {
       request.reply(GetSpikes(request));
+    } else if (request.method() == "GET" &&
+               request.relative_uri().path() == "/multimeter_measurement") {
+      request.reply(GetMultimeterMeasurement(request));
+    } else if (request.method() == "GET" &&
+               request.relative_uri().path() == "/current_simulation_time") {
+      request.reply(GetCurrentSimulationTime(request));
     } else {
       std::cerr << "Invalid request: " << request.to_string() << "\n";
       request.reply(web::http::status_codes::NotFound);
@@ -19,19 +34,170 @@ HttpServer::HttpServer(web::http::uri address, DataStorage* storage)
   std::cout << "HTTP server is listening...\n";
 }
 
+web::http::http_response HttpServer::GetCurrentSimulationTime(
+    const web::http::http_request& request) {
+  web::http::http_response response(web::http::status_codes::OK);
+  web::json::value simulation_time = storage_->GetCurrentSimulationTime();
+  response.set_body(simulation_time);
+  return response;
+}
+
 web::http::http_response HttpServer::GetSpikes(
     const web::http::http_request& request) {
+  const auto parameters = web::uri::split_query(request.request_uri().query());
+
   web::http::http_response response(web::http::status_codes::OK);
-  const auto spikes = storage_->GetSpikes();
-  web::json::value gids = web::json::value::array(spikes.size());
-  web::json::value simulation_steps = web::json::value::array(spikes.size());
-  for (size_t i = 0; i < spikes.size(); ++i) {
-    gids[i] = spikes[i].gid;
-    simulation_steps[i] = spikes[i].simulation_step;
+  auto spikes = storage_->GetSpikes();
+
+  const auto from = parameters.find("from");
+  const auto to = parameters.find("to");
+  const auto population = parameters.find("population");
+
+    std::unordered_set<uint64_t> population_node_ids;
+  if (population != parameters.end()) {
+    pqxx::connection connection(database_uri_);
+    pqxx::work txn(connection);
+    const auto population_node_ids_result = txn.exec(
+        "SELECT id FROM nest_neuron WHERE nest_neuron.population_id = " +
+        population->second);
+    txn.commit();
+
+    population_node_ids.reserve(population_node_ids_result.size());
+
+    for (const auto& node_id : population_node_ids_result) {
+      population_node_ids.insert(node_id[0].as<uint64_t>());
+    }
+
+    spikes.erase(std::remove_if(spikes.begin(), spikes.end(), [&population_node_ids](const Spike& spike) {
+      return population_node_ids.count(spike.gid) == 0;
+    }), spikes.end());
+  }
+
+  const auto spike_happened_before = [](const Spike& spike,
+                                        double simulation_time) {
+    return spike.simulation_time < simulation_time;
+  };
+
+  auto spikes_begin = spikes.begin();
+  auto spikes_end = spikes.end();
+  if (from != parameters.end()) {
+    const auto from_number = std::stoll(from->second);
+    spikes_begin = std::lower_bound(spikes.begin(), spikes.end(), from_number,
+                                    spike_happened_before);
+  }
+
+  if (to != parameters.end()) {
+    const auto to_number = std::stoll(to->second);
+    spikes_end = std::lower_bound(spikes.begin(), spikes.end(), to_number,
+                                  spike_happened_before);
   }
+
+
+  const auto element_count = spikes_end - spikes_begin;
+  web::json::value gids = web::json::value::array(element_count);
+  web::json::value simulation_times = web::json::value::array(element_count);
+
+  {
+    size_t index = 0;
+    for (auto spike = spikes_begin; spike != spikes_end; ++spike, ++index) {
+      gids[index] = spike->gid;
+      simulation_times[index] = spike->simulation_time;
+    }
+  }
+
   response.set_body(web::json::value::object(
-      {{"simulation_steps", simulation_steps}, {"neuron_ids", gids}}));
+      {{"simulation_times", simulation_times}, {"gids", gids}}));
   return response;
 }
 
+web::http::http_response HttpServer::GetMultimeterMeasurement(
+    const web::http::http_request& request) {
+  web::http::http_response response(web::http::status_codes::OK);
+  web::json::value body = web::json::value::object();
+
+  const auto parameters = web::uri::split_query(request.request_uri().query());
+  const auto parameter_multimeter_id = parameters.find("multimeter_id");
+  const auto parameter_attribute = parameters.find("attribute");
+  const auto parameter_from = parameters.find("from");
+  const auto parameter_to = parameters.find("to");
+  const auto parameter_gids = parameters.find("gids");
+  const auto parameter_offset = parameters.find("offset");
+  const auto parameter_limit = parameters.find("limit");
+
+  const auto multimeter_id = std::stoll(parameter_multimeter_id->second);
+  const auto attribute = parameter_attribute->second;
+
+  auto filter_gids = std::vector<std::uint64_t>();
+  if (parameter_gids != parameters.end()) {
+    std::regex regex{R"([\s,]+)"};
+    std::sregex_token_iterator it{parameter_gids->second.begin(),
+                                  parameter_gids->second.end(), regex, -1};
+    std::vector<std::string> filter_gid_strings{it, {}};
+    std::transform(filter_gid_strings.begin(), filter_gid_strings.end(),
+                   std::back_inserter(filter_gids),
+                   [](const std::string& str) { return std::stoll(str); });
+  }
+  std::cout << "Filter GID count: " << filter_gids.size() << "\n";
+
+  const auto measurements = storage_->GetMultimeterMeasurements();
+  if (measurements.find(multimeter_id) != measurements.end() &&
+      measurements.at(multimeter_id).find(attribute) !=
+          measurements.at(multimeter_id).end()) {
+    auto& measurement = measurements.at(multimeter_id).at(attribute);
+    auto& simulation_times = measurement.simulation_times;
+    auto& gids = measurement.gids;
+    auto& values = measurement.values;
+
+    auto simulation_times_begin =
+        parameter_from == parameters.end()
+            ? simulation_times.begin()
+            : std::lower_bound(simulation_times.begin(), simulation_times.end(),
+                               std::stoll(parameter_from->second));
+    auto simulation_times_end =
+        parameter_to == parameters.end()
+            ? simulation_times.end()
+            : std::lower_bound(simulation_times.begin(), simulation_times.end(),
+                               std::stoll(parameter_to->second));
+    if (parameter_offset != parameters.end())
+      simulation_times_begin += std::stoll(parameter_offset->second);
+    if (parameter_limit != parameters.end())
+      simulation_times_end =
+          simulation_times_begin + std::stoll(parameter_limit->second);
+    auto simulation_times_subset = std::vector<web::json::value>(
+        simulation_times_begin, simulation_times_end);
+    std::size_t simulation_start_index =
+        std::distance(simulation_times.begin(), simulation_times_begin);
+    std::size_t simulation_end_index =
+        std::distance(simulation_times.begin(), simulation_times_end);
+
+    auto gids_begin = filter_gids.empty() ? gids.begin() : filter_gids.begin();
+    auto gids_end = filter_gids.empty() ? gids.end() : filter_gids.end();
+    auto gids_subset = std::vector<web::json::value>(gids_begin, gids_end);
+    auto gid_indices = std::vector<std::size_t>();
+    if (!filter_gids.empty())
+      for (auto& filter_gid : filter_gids)
+        gid_indices.push_back(std::distance(
+            gids.begin(),
+            std::lower_bound(gids.begin(), gids.end(), filter_gid)));
+    else {
+      gid_indices.resize(gids.size());
+      std::iota(gid_indices.begin(), gid_indices.end(), 0);
+    }
+
+    auto values_subset = std::vector<web::json::value>(
+        simulation_times_subset.size() * gids_subset.size());
+    for (std::size_t t = 0, vt = simulation_start_index;
+         vt < simulation_end_index; ++t, ++vt)
+      for (std::size_t g = 0; g < gid_indices.size(); ++g)
+        values_subset[t * gids_subset.size() + g] =
+            values[vt * gids.size() + gid_indices[g]];
+
+    body["simulation_times"] = web::json::value::array(simulation_times_subset);
+    body["gids"] = web::json::value::array(gids_subset);
+    body["values"] = web::json::value::array(values_subset);
+  }
+
+  response.set_body(body);
+  return response;
+}
 }  // namespace insite
diff --git a/http_server.hpp b/http_server.hpp
index bca17999f0c5048137e476a8ee16fa40e1b6250e..90f0bbd92201d251fb72b343fcd1963fd0c55bca 100644
--- a/http_server.hpp
+++ b/http_server.hpp
@@ -3,6 +3,7 @@
 
 #include <cpprest/http_listener.h>
 #include <string>
+#include <pqxx/pqxx>
 
 namespace insite {
 
@@ -10,13 +11,19 @@ class DataStorage;
 
 class HttpServer {
  public:
-  HttpServer(web::http::uri address, DataStorage* storage);
+  HttpServer(web::http::uri address, DataStorage* storage, std::string database_uri);
 
  private:
-  DataStorage* storage_;
   web::http::experimental::listener::http_listener http_listener_;
+  DataStorage* storage_;
+  std::string database_uri_;
+
+  web::http::http_response GetCurrentSimulationTime(const web::http::http_request& request);
 
   web::http::http_response GetSpikes(const web::http::http_request& request);
+  
+  web::http::http_response GetMultimeterMeasurement(
+    const web::http::http_request& request);
 };
 
 }  // namespace insite
diff --git a/insite-nest-module.tar b/insite-nest-module.tar
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/neuron_info.hpp b/neuron_info.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..19bf4e3eda6e8de17159f44f177044b446e1118d
--- /dev/null
+++ b/neuron_info.hpp
@@ -0,0 +1,21 @@
+#ifndef NEURON_INFO_HPP
+#define NEURON_INFO_HPP
+
+#include <vector>
+
+// Includes from nest kernel:
+#include <node_collection.h>
+
+namespace insite {
+
+struct NeuronInfo {
+  bool operator<(const NeuronInfo& that) const { return gid < that.gid; }
+
+  nest::index gid;
+  nest::NodeCollectionPTR gid_collection;
+  std::vector<double> position;
+};
+
+}  // namespace insite
+
+#endif
diff --git a/recording_backend_insite.cpp b/recording_backend_insite.cpp
index c94e87386d29d51fe3d74301645d17e4d90d023b..63a59929870df327f5d3d76b46f6bff45645ca28 100644
--- a/recording_backend_insite.cpp
+++ b/recording_backend_insite.cpp
@@ -1,20 +1,54 @@
+#include <stdexcept>
+#include <string>
+
 // Includes from libnestutil:
 #include "compose.hpp"
 
 // Includes from nestkernel:
+#include "kernel_manager.h"
 #include "recording_device.h"
 #include "vp_manager_impl.h"
 
+// Includes from topology:
+#include "topology.h"
+
 // Includes from sli:
 #include "dictutils.h"
-
 #include "recording_backend_insite.h"
 
 namespace insite {
 
+namespace {
+
+std::string ReadDatabaseHost() {
+  std::ifstream host_file("database_host.txt");
+  if (host_file.is_open()) {
+    std::string database_host;
+    std::getline(host_file, database_host);
+    return database_host;
+  } else {
+    return "database";
+  }
+}
+
+}  // namespace
+
 RecordingBackendInsite::RecordingBackendInsite()
     : data_storage_("tgest"),
-      http_server_("http://0.0.0.0:8000", &data_storage_) {}
+      database_connection_("postgresql://postgres@" + ReadDatabaseHost()),
+      http_server_("http://0.0.0.0:" + get_port_string(), &data_storage_, "postgresql://postgres@" + ReadDatabaseHost()) {
+  pqxx::work txn(database_connection_);
+  simulation_node_id_ = txn.exec1(
+                               "INSERT INTO nest_simulation_node (address) "
+                               "VALUES ('http://insite-nest-module:" +
+                               get_port_string() +
+                               "') "
+                               "RETURNING id;")[0]
+                            .as<int>();
+  std::cout << "Simulation node registered to database. Node ID: "
+            << simulation_node_id_ << std::endl;
+  txn.commit();
+}
 
 RecordingBackendInsite::~RecordingBackendInsite() throw() {}
 
@@ -29,6 +63,11 @@ void RecordingBackendInsite::finalize() {
 void RecordingBackendInsite::enroll(const nest::RecordingDevice& device,
                                     const DictionaryDatum& params) {
   std::cout << "RecordingBackendInsite::enroll(" << device.get_label() << ")\n";
+
+  if (device.get_type() == nest::RecordingDevice::MULTIMETER) {
+    auto id = device.get_node_id();
+    multimeter_infos_.emplace(std::make_pair(id, MultimeterInfo{id, true}));
+  }
 }
 
 void RecordingBackendInsite::disenroll(const nest::RecordingDevice& device) {
@@ -41,6 +80,37 @@ void RecordingBackendInsite::set_value_names(
     const std::vector<Name>& double_value_names,
     const std::vector<Name>& long_value_names) {
   std::cout << "RecordingBackendInsite::set_value_names()\n";
+
+  if (device.get_type() == nest::RecordingDevice::MULTIMETER) {
+    auto& multimeter = multimeter_infos_.at(device.get_node_id());
+
+    std::stringstream multimeter_query;
+    multimeter_query << "INSERT INTO nest_multimeter (id, attributes) "
+                     << "VALUES (" << device.get_node_id() << ",\'{";
+
+    bool first = true;
+    for (auto& name : double_value_names) {
+      const auto& name_string = name.toString();
+      multimeter.double_attributes.push_back(name_string);
+
+      multimeter_query << (first ? "" : ",") << '\"' << name_string << "\"";
+      first = false;
+    }
+    for (auto& name : long_value_names) {
+      const auto& name_string = name.toString();
+      multimeter.long_attributes.push_back(name_string);
+
+      multimeter_query << (first ? "" : ",") << '\"' << name_string << "\"";
+      first = false;
+    }
+    multimeter_query << "}\') ON CONFLICT DO NOTHING;";
+
+    multimeter.needs_update = true;
+
+    pqxx::work txn(database_connection_);
+    txn.exec0(multimeter_query.str());
+    txn.commit();
+  }
 }
 
 void RecordingBackendInsite::prepare() {}
@@ -57,15 +127,146 @@ void RecordingBackendInsite::post_run_hook() {
   std::cout << "RecordingBackendInsite::post_run_hook()\n";
 }
 
-void RecordingBackendInsite::post_step_hook() {}
+void RecordingBackendInsite::post_step_hook() {
+  // // Send simulation time
+  // {
+  //   pqxx::work txn(database_connection_);
+  //   txn.exec0(
+  //       "UPDATE nest_simulation_node "
+  //       "SET current_simulation_time = " +
+  //       std::to_string(latest_simulation_time_) +
+  //       ""
+  //       "WHERE id = " +
+  //       std::to_string(simulation_node_id_));
+  //   txn.commit();
+  // }
+  data_storage_.SetCurrentSimulationTime(latest_simulation_time_);
+
+  if (new_neuron_infos_.size() > 0) {
+    std::stringstream neuron_query;
+    neuron_query << "INSERT INTO nest_neuron (id, simulation_node_id, "
+                    "population_id, position) "
+                 << "VALUES ";
+    for (auto& neuron_info : new_neuron_infos_) {
+      const bool first = neuron_info.gid == new_neuron_infos_[0].gid;
+      if (!first) {
+        neuron_query << ",";
+      }
+
+      uint64_t population_id = 0;
+      for (const nest::NodeIDTriple& node_id_triple :
+           *neuron_info.gid_collection.get()) {
+        population_id ^= node_id_triple.node_id * 938831;
+      }
+
+      neuron_query << "(" << neuron_info.gid << "," << simulation_node_id_
+                   << "," << population_id % 0x800000;
+
+      const auto position_size = neuron_info.position.size();
+      if (position_size > 0) {
+        assert(position_size <= 3);
+        neuron_query << ",\'{";
+        for (size_t i = 0; i < position_size; ++i) {
+          if (i > 0) {
+            neuron_query << ",";
+          }
+          neuron_query << neuron_info.position[i];
+        }
+        neuron_query << "}\'";
+      } else {
+        neuron_query << ",NULL";
+      }
+      neuron_query << ")";
+    }
+    neuron_query << ";";
+
+    pqxx::work txn(database_connection_);
+    txn.exec0(neuron_query.str());
+    txn.commit();
+
+    neuron_infos_.insert(neuron_infos_.end(), new_neuron_infos_.begin(),
+                         new_neuron_infos_.end());
+    std::sort(neuron_infos_.begin(), neuron_infos_.end());
+    new_neuron_infos_.clear();
+  }
+
+  // Send multimeter info
+  // for (auto& kvp : multimeter_infos_) {
+  //   auto& multimeter = kvp.second;
+  //   if (!multimeter.needs_update) continue;
+  //   multimeter.needs_update = false;
+
+  //   if (multimeter.gids.size() > 0) {
+  //     std::stringstream neuron_multimeter_query;
+  //     neuron_multimeter_query
+  //         << "INSERT INTO nest_neuron_multimeter (neuron_id, multimeter_id) "
+  //         << "VALUES ";
+
+  //     for (const auto& neuron_id : multimeter.gids) {
+  //       const bool first = neuron_id == multimeter.gids[0];
+  //       neuron_multimeter_query << (first ? "" : ",") << "(" << neuron_id << ","
+  //                               << multimeter.device_id << ")";
+  //     }
+
+  //     neuron_multimeter_query << " ON CONFLICT DO NOTHING;";
+
+  //     pqxx::work txn(database_connection_);
+  //     txn.exec0(neuron_multimeter_query.str());
+  //     txn.commit();
+  //   }
+  // }
+}
 
 void RecordingBackendInsite::write(const nest::RecordingDevice& device,
                                    const nest::Event& event,
                                    const std::vector<double>& double_values,
                                    const std::vector<long>& long_values) {
+  const auto sender_gid = event.get_sender_node_id();
+  const auto time_stamp = event.get_stamp().get_ms();
   if (device.get_type() == nest::RecordingDevice::SPIKE_DETECTOR) {
-    data_storage_.AddSpike(event.get_stamp().get_steps(),
-                           event.get_sender_gid());
+    data_storage_.AddSpike(time_stamp, sender_gid);
+  }
+  if (device.get_type() == nest::RecordingDevice::MULTIMETER) {
+    auto device_id = device.get_node_id();
+    auto& multimeter = multimeter_infos_.at(device_id);
+    auto& gids = multimeter.gids;
+
+    // If the measurement is from a GID we previously do not know, add.
+    if (!binary_search(gids.begin(), gids.end(), sender_gid)) {
+      gids.insert(std::lower_bound(gids.begin(), gids.end(), sender_gid),
+                  sender_gid);
+      multimeter.needs_update = true;
+    }
+
+    for (std::size_t i = 0; i < double_values.size(); ++i)
+      data_storage_.AddMultimeterMeasurement(
+          device_id, multimeter.double_attributes[i], time_stamp, sender_gid,
+          double_values[i]);
+    for (std::size_t i = 0; i < long_values.size(); ++i)
+      data_storage_.AddMultimeterMeasurement(
+          device_id, multimeter.long_attributes[i], time_stamp, sender_gid,
+          double(long_values[i]));
+  }
+  latest_simulation_time_ = std::max(latest_simulation_time_, time_stamp);
+
+  NeuronInfo neuron_info;
+  neuron_info.gid = sender_gid;
+  if (!binary_search(neuron_infos_.begin(), neuron_infos_.end(), neuron_info) &&
+      !binary_search(new_neuron_infos_.begin(), new_neuron_infos_.end(),
+                     neuron_info)) {
+    neuron_info.gid_collection = event.get_sender().get_nc();
+
+    const auto layer = nest::get_layer(neuron_info.gid_collection);
+    if (layer.get()) {
+      neuron_info.position = layer->get_position_vector(
+          neuron_info.gid_collection->find(sender_gid));
+    }
+
+    new_neuron_infos_.insert(
+        std::lower_bound(new_neuron_infos_.begin(), new_neuron_infos_.end(),
+                         neuron_info),
+        neuron_info);
+    data_storage_.AddNeuronId(neuron_info.gid);
   }
 }
 
@@ -92,4 +293,7 @@ void RecordingBackendInsite::get_device_status(
   std::cout << "RecordingBackendInsite::get_device_status()\n";
 }
 
+std::string RecordingBackendInsite::get_port_string() const {
+  return std::to_string(8000 + nest::kernel().mpi_manager.get_rank());
+}
 }  // namespace insite
diff --git a/recording_backend_insite.h b/recording_backend_insite.h
index 38985cf67628492c8805d7aef2102f70d553fe83..93c071e3d9e575e041af79e9cac5ac5c8026b6c9 100644
--- a/recording_backend_insite.h
+++ b/recording_backend_insite.h
@@ -1,10 +1,17 @@
 #ifndef RECORDING_BACKEND_INSITE_H
 #define RECORDING_BACKEND_INSITE_H
 
+#include <unordered_map>
+#include <cpprest/http_client.h>
+#include <pqxx/pqxx>
+
 #include "data_storage.hpp"
 #include "http_server.hpp"
-
 #include "recording_backend.h"
+#include "nest_types.h"
+#include "node_collection.h"
+
+#include "neuron_info.hpp"
 
 namespace insite {
 
@@ -52,8 +59,16 @@ class RecordingBackendInsite : public nest::RecordingBackend {
                          DictionaryDatum& params) const override;
 
  private:
+  std::string get_port_string() const;
+
   DataStorage data_storage_;
+  pqxx::connection database_connection_;
   HttpServer http_server_;
+  int simulation_node_id_;
+  std::vector<NeuronInfo> neuron_infos_;
+  std::vector<NeuronInfo> new_neuron_infos_;
+  std::unordered_map<nest::index, MultimeterInfo> multimeter_infos_;
+  double latest_simulation_time_ = 0;
 };
 
 }  // namespace insite
diff --git a/test/test_module.py b/test/test_module.py
new file mode 100644
index 0000000000000000000000000000000000000000..e16fe6ce14c114a94c94894be652fb8dc449f962
--- /dev/null
+++ b/test/test_module.py
@@ -0,0 +1,11 @@
+import requests
+
+def test_spikes(nest_simulation):
+    r = requests.get("http://localhost:8000/spikes")
+    spikes = r.json()
+    assert(len(spikes['gids']) == len(spikes['simulation_times']))
+    
+    previous_time = 0
+    for time in spikes['simulation_times']:
+        assert(time >= previous_time)
+        previous_time = time
diff --git a/wait-for.sh b/wait-for.sh
new file mode 100755
index 0000000000000000000000000000000000000000..b1e34c1c0d17809376acfcf05302bd6b0b4897b2
--- /dev/null
+++ b/wait-for.sh
@@ -0,0 +1,79 @@
+#!/bin/sh
+
+TIMEOUT=15
+QUIET=0
+
+echoerr() {
+  if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi
+}
+
+usage() {
+  exitcode="$1"
+  cat << USAGE >&2
+Usage:
+  $cmdname host:port [-t timeout] [-- command args]
+  -q | --quiet                        Do not output any status messages
+  -t TIMEOUT | --timeout=timeout      Timeout in seconds, zero for no timeout
+  -- COMMAND ARGS                     Execute command with args after the test finishes
+USAGE
+  exit "$exitcode"
+}
+
+wait_for() {
+  for i in `seq $TIMEOUT` ; do
+    nc -z "$HOST" "$PORT" > /dev/null 2>&1
+    
+    result=$?
+    if [ $result -eq 0 ] ; then
+      if [ $# -gt 0 ] ; then
+        exec "$@"
+      fi
+      exit 0
+    fi
+    sleep 1
+  done
+  echo "Operation timed out" >&2
+  exit 1
+}
+
+while [ $# -gt 0 ]
+do
+  case "$1" in
+    *:* )
+    HOST=$(printf "%s\n" "$1"| cut -d : -f 1)
+    PORT=$(printf "%s\n" "$1"| cut -d : -f 2)
+    shift 1
+    ;;
+    -q | --quiet)
+    QUIET=1
+    shift 1
+    ;;
+    -t)
+    TIMEOUT="$2"
+    if [ "$TIMEOUT" = "" ]; then break; fi
+    shift 2
+    ;;
+    --timeout=*)
+    TIMEOUT="${1#*=}"
+    shift 1
+    ;;
+    --)
+    shift
+    break
+    ;;
+    --help)
+    usage 0
+    ;;
+    *)
+    echoerr "Unknown argument: $1"
+    usage 1
+    ;;
+  esac
+done
+
+if [ "$HOST" = "" -o "$PORT" = "" ]; then
+  echoerr "Error: you need to provide a host and port to test."
+  usage 2
+fi
+
+wait_for "$@"
\ No newline at end of file