diff --git a/Makefile b/Makefile
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@
 
 start: db build test-nginx
 
-test: db 
+test: 
 	./npm.sh run test
 
 watch: 
diff --git a/lib/app/app.js b/lib/app/app.js
--- a/lib/app/app.js
+++ b/lib/app/app.js
@@ -17,6 +17,7 @@
 
 const WwwServerFactory = locreq("lib/http/http.js");
 const DatastoreMongoFactory = locreq("lib/datastore/db.js");
+const MetadataFactory = locreq("lib/app/metadata.js");
 
 const load_base_chips = locreq("lib/app/load-base-chips");
 const default_config = locreq("default_config.json");
@@ -26,6 +27,7 @@
 
 class App {
 	constructor(custom_config, manifest) {
+		this.status = "stopped";
 		this.Sealious = Sealious;
 		this.setupEventEmitter();
 
@@ -55,6 +57,7 @@
 		load_base_chips(this);
 
 		this.Datastore = new DatastoreMongoFactory(this);
+		this.Metadata = MetadataFactory(this);
 
 		this.FieldType = Sealious.FieldType.bind(Sealious.FieldType, this);
 		this.Collection = Sealious.Collection.bind(Sealious.Collection, this);
@@ -100,6 +103,7 @@
 	}
 
 	async start() {
+		this.status = "starting";
 		assert(
 			["dev", "production"].includes(
 				this.ConfigManager.get("core.environment")
@@ -110,12 +114,17 @@
 		await this.Datastore.start();
 		await this.Mail.init();
 		await this.ChipManager.start_chips();
+		await this.emit("start");
+		this.status = "running";
 		return this;
 	}
 
 	async stop() {
+		this.status = "stopping";
+		await this.emit("stop");
 		await this.WwwServer.stop();
 		await this.Datastore.stop();
+		this.status = "stopped";
 	}
 }
 
diff --git a/lib/app/base-chips/collections/password-reset-intents.subtest.js b/lib/app/base-chips/collections/password-reset-intents.subtest.js
--- a/lib/app/base-chips/collections/password-reset-intents.subtest.js
+++ b/lib/app/base-chips/collections/password-reset-intents.subtest.js
@@ -2,28 +2,10 @@
 const axios = require("axios");
 const assert = require("assert");
 const { promise_timeout } = locreq("test_utils");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("password-reset-intents", () => {
-	let app = null;
-	const port = 8888;
-	const base_url = `http://localhost:${port}/api/v1`;
-	let smtp_api = null;
-	before(() => (smtp_api = TestApp.ConfigManager.get("tests.smtp_api_url")));
-
-	beforeEach(async () => {
-		app = new Sealious.App(
-			{
-				"www-server": { port: 8888 },
-				upload_path: "/dev/null",
-				logger: { level: "emerg" },
-				core: { environment: "production" },
-				smtp: TestApp.ConfigManager.get("smtp"),
-				datastore_mongo: TestApp.ConfigManager.get("datastore_mongo"),
-			},
-			TestApp.manifest
-		);
-
-		await app.start();
+	async function create_a_user(app) {
 		await app.run_action(
 			new app.Sealious.SuperContext(),
 			["collections", "users"],
@@ -34,71 +16,75 @@
 				password: "password",
 			}
 		);
-		await axios.delete(`${smtp_api}/messages`);
-	});
+	}
 
-	it("tells you if the email address doesn't exist", async () => {
-		try {
-			await axios.post(`${base_url}/collections/password-reset-intents`, {
-				email: "fake@example.com",
-			});
-		} catch (e) {
-			assert.equal(
-				e.response.data.data.email.message,
-				"No users with email set to fake@example.com"
-			);
-			return;
-		}
-		throw new Error("it didn't throw");
-	});
-
-	it("allows anyone to create an intent, if the email exists", async () => {
-		const data = (await axios.post(
-			`${base_url}/collections/password-reset-intents`,
-			{
-				email: "user@example.com",
+	it("tells you if the email address doesn't exist", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			try {
+				await axios.post(
+					`${base_url}/api/v1/collections/password-reset-intents`,
+					{
+						email: "fake@example.com",
+					}
+				);
+			} catch (e) {
+				assert.equal(
+					e.response.data.data.email.message,
+					"No users with email set to fake@example.com"
+				);
+				return;
 			}
-		)).data;
-		assert.deepEqual(data.body, {
-			email: "user@example.com",
-			token: "it's a secret to everybody",
-		});
-	});
+			throw new Error("it didn't throw");
+		}));
 
-	it("tells you if the email address is malformed", async () => {
-		try {
-			await axios.post(`${base_url}/collections/password-reset-intents`, {
-				email: "incorrect-address",
+	it("allows anyone to create an intent, if the email exists", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			await create_a_user(app);
+			const data = (await axios.post(
+				`${base_url}/api/v1/collections/password-reset-intents`,
+				{
+					email: "user@example.com",
+				}
+			)).data;
+			assert.deepEqual(data.body, {
+				email: "user@example.com",
+				token: "it's a secret to everybody",
 			});
-		} catch (e) {
-			assert.equal(
-				e.response.data.data.email.message,
-				"incorrect-address is a not valid e-mail address."
-			);
-			return;
-		}
-		throw new Error("it didn't throw");
-	});
+		}));
 
-	it("sends an email with the reset password link", async () => {
-		const data = (await axios.post(
-			`${base_url}/collections/password-reset-intents`,
-			{
-				email: "user@example.com",
+	it("tells you if the email address is malformed", async () =>
+		with_running_app(async ({ base_url }) => {
+			try {
+				await axios.post(
+					`${base_url}/api/v1/collections/password-reset-intents`,
+					{
+						email: "incorrect-address",
+					}
+				);
+			} catch (e) {
+				assert.equal(
+					e.response.data.data.email.message,
+					"incorrect-address is a not valid e-mail address."
+				);
+				return;
 			}
-		)).data;
-		const messages = (await axios.get(`${smtp_api}/messages`)).data;
-		assert.equal(messages.length, 1);
-		assert.equal(messages[0].recipients.length, 1);
-		assert.equal(messages[0].recipients[0], "<user@example.com>");
-	});
+			throw new Error("it didn't throw");
+		}));
 
-	afterEach(async () => {
-		await Promise.all(
-			app.ChipManager.get_all_collections().map(collection_name =>
-				app.Datastore.remove(collection_name, {}, "just_one" && false)
-			)
-		);
-		await app.stop();
-	});
+	it("sends an email with the reset password link", async () =>
+		with_running_app(async ({ app, base_url, mail_api }) => {
+			await create_a_user(app);
+			const data = (await axios.post(
+				`${base_url}/api/v1/collections/password-reset-intents`,
+				{
+					email: "user@example.com",
+				}
+			)).data;
+			const messages = (await mail_api.get_messages()).filter(
+				message => message.recipients[0] == "<user@example.com>"
+			);
+			assert(messages.length, 1);
+			assert.equal(messages[0].recipients.length, 1);
+			assert.equal(messages[0].recipients[0], "<user@example.com>");
+		}));
 });
diff --git a/lib/app/base-chips/field-types/field-types.test.js b/lib/app/base-chips/field-types/field-types.test.js
--- a/lib/app/base-chips/field-types/field-types.test.js
+++ b/lib/app/base-chips/field-types/field-types.test.js
@@ -1,3 +1,4 @@
 describe("field types", () => {
 	require("./single_reference.subtest.js");
+	require("./reverse-single-reference.subtest.js");
 });
diff --git a/lib/app/base-chips/field-types/reverse-single-reference.js b/lib/app/base-chips/field-types/reverse-single-reference.js
new file mode 100644
--- /dev/null
+++ b/lib/app/base-chips/field-types/reverse-single-reference.js
@@ -0,0 +1,213 @@
+"use strict";
+const locreq = require("locreq")(__dirname);
+const Promise = require("bluebird");
+const Errors = locreq("lib/response/error.js");
+const assert = require("assert");
+
+function params_to_cache_key(collection, field_name, params) {
+	return `${collection.name}___${field_name}-reverse-single-reference(${
+		params.collection.name
+	},${params.collection.field_name}).last_update`;
+}
+
+async function update_cache(
+	app,
+	collection,
+	field_name,
+	params,
+	resource_ids = undefined
+) {
+	let pipeline;
+	if (resource_ids) {
+		assert(Array.isArray(resource_ids));
+		pipeline = [
+			{ $match: { [`body.${params.field_name}`]: { $in: resource_ids } } },
+		];
+	} else {
+		pipeline = [];
+	}
+	pipeline.push({
+		$group: {
+			_id: `$body.${params.field_name}`,
+			referenced_by: { $push: `$sealious_id` },
+		},
+	});
+	const to_update = await app.Datastore.aggregate(
+		params.collection.name,
+		pipeline
+	);
+	if (resource_ids) {
+		for (let resource_id of resource_ids) {
+			if (to_update.filter(e => e._id == resource_id).length === 0) {
+				to_update.push({ _id: resource_id, referenced_by: [] });
+			}
+		}
+	}
+	for (let entry of to_update) {
+		await app.Datastore.update(
+			collection.name,
+			{ sealious_id: entry._id },
+			{ $set: { [`body.${field_name}`]: entry.referenced_by } }
+		);
+	}
+}
+
+const reverse_single_reference_factory = app => {
+	return {
+		name: "reverse-single-reference",
+
+		get_description: function() {
+			return "Shows which resources from given collection point to this resource in a given field.";
+		},
+
+		get_default_value: async () => [],
+
+		is_proper_value: function(context, params, new_value) {
+			return context.is_super
+				? Promise.resolve()
+				: Promise.reject("This is a read-only field");
+		},
+
+		filter_to_query: async function(context, params, field_filter) {
+			if (typeof field_filter === "object") {
+				const matches = await app.run_action(
+					context,
+					["collections", params.collection.name],
+					"show",
+					{
+						filter: field_filter,
+						[params.field_name]: "asdfasdfasdfasukyfgbyausdgbrfdaye",
+					}
+				);
+				const ids = matches.map(resource => resource.id);
+				return {
+					$in: ids,
+				};
+			} else {
+				return {
+					$eq: field_filter,
+				};
+			}
+		},
+
+		format: function(context, params, decoded_value, format) {
+			// format can be "expand" or "deep-expand:<depth>", like "deep-expand:3"
+			if (!format) {
+				return decoded_value; // just the IDs
+			}
+
+			const format_params = format.split(":");
+
+			if (format_params[0] === "expand" || format_params[0] === "deep-expand") {
+				if (decoded_value === undefined) {
+					return undefined;
+				}
+				const query_format = {};
+				if (format_params[0] === "deep-expand" && format_params[1] > 1) {
+					for (const field_name in params.collection.fields) {
+						const field = params.collection.fields[field_name];
+						if (field.type.name === "single_reference") {
+							query_format[field_name] = `deep-expand:${parseInt(
+								format_params[1]
+							) - 1}`;
+						}
+					}
+				}
+				const resource_ids = decoded_value;
+				return Promise.map(resource_ids, async resource_id =>
+					app.run_action(
+						context,
+						["collections", params.collection.name, resource_id],
+						"show",
+						{ format: query_format }
+					)
+				);
+			} else {
+				return decoded_value;
+			}
+		},
+
+		init: async (collection, field_name, params) => {
+			assert(
+				params.collection instanceof app.Sealious.Collection,
+				"'params.collection' should be an instance of Collection"
+			);
+			assert(
+				params.collection.fields[params.field_name],
+				`Collection '${
+					params.collection.name
+				}' does not contain a field named ${params.field_name}.`
+			);
+			app.on("start", async () => {
+				const last_modified_resource_in_reference_collection = (await app.run_action(
+					new app.Sealious.SuperContext(),
+					["collections", params.collection.name],
+					"show",
+					{
+						sort: { "last_modified_context.timestamp": "desc" },
+						pagination: { items: 1 },
+					}
+				))[0];
+
+				if (last_modified_resource_in_reference_collection) {
+					const last_modified_resource_timestamp =
+						last_modified_resource_in_reference_collection.last_modified_context
+							.timestamp;
+					const last_field_cache_update =
+						(await app.Metadata.get(
+							params_to_cache_key(collection, field_name, params)
+						)) || 0;
+					if (last_modified_resource_timestamp > last_field_cache_update) {
+						await update_cache(app, collection, field_name, params);
+						await app.Metadata.set(
+							params_to_cache_key(collection, field_name, params),
+							Date.now()
+						);
+					}
+				}
+			});
+			app.on(
+				new RegExp(`post:collections\.${params.collection.name}:create`),
+				async (path, event_params, resource) => {
+					const referenced_id = resource.body[params.field_name];
+					await update_cache(app, collection, field_name, params, [
+						referenced_id,
+					]);
+				}
+			);
+			app.on(
+				new RegExp(`post:collections\.${params.collection.name}\..*:delete`),
+				async (path, event_params, resource) => {
+					const deleted_id = path[2];
+					const affected = await app.Datastore.find(collection.name, {
+						[`body.${field_name}`]: deleted_id,
+					});
+					const affected_ids = affected.map(document => document.sealious_id);
+					await update_cache(app, collection, field_name, params, affected_ids);
+				}
+			);
+			app.on(
+				new RegExp(`post:collections\.${params.collection.name}\..*:edit`),
+				async (path, event_params, resource) => {
+					if (!event_params.hasOwnProperty(params.field_name)) return;
+					const edited_id = path[2];
+					const no_longer_referenced = await app.Datastore.find(
+						collection.name,
+						{
+							[`body.${field_name}`]: edited_id,
+						}
+					);
+					const affected_ids = no_longer_referenced.map(
+						document => document.sealious_id
+					);
+					if (event_params[params.field_name]) {
+						affected_ids.push(event_params[params.field_name]);
+					}
+					await update_cache(app, collection, field_name, params, affected_ids);
+				}
+			);
+		},
+	};
+};
+
+module.exports = reverse_single_reference_factory;
diff --git a/lib/app/base-chips/field-types/reverse-single-reference.subtest.js b/lib/app/base-chips/field-types/reverse-single-reference.subtest.js
new file mode 100644
--- /dev/null
+++ b/lib/app/base-chips/field-types/reverse-single-reference.subtest.js
@@ -0,0 +1,197 @@
+const assert = require("assert");
+const locreq = require("locreq")(__dirname);
+const axios = require("axios");
+const { create_resource_as } = locreq("test_utils");
+const { with_stopped_app, with_running_app } = locreq(
+	"test_utils/with-test-app.js"
+);
+const DatastoreMongoFactory = locreq("lib/datastore/db.js");
+
+describe("reverse-single-reference", () => {
+	async function create_referencing_collections(app, with_reverse) {
+		const A = app.createChip(app.Sealious.Collection, {
+			name: "A",
+			fields: [
+				{
+					name: "reference_to_b",
+					type: "single_reference",
+					params: { collection: "B" },
+				},
+				{
+					name: "pairity",
+					type: "text",
+				},
+			],
+		});
+		const B = app.createChip(app.Sealious.Collection, {
+			name: "B",
+			fields: [{ name: "number", type: "int" }],
+		});
+		if (with_reverse) {
+			B.add_field({
+				name: "references_in_a",
+				type: "reverse-single-reference",
+				params: { collection: A, field_name: "reference_to_b" },
+			});
+		}
+	}
+
+	async function create_resources(app) {
+		const numbers = [1, 2, 3];
+		const bs = [];
+		for (let number of numbers) {
+			const new_b = await app.run_action(
+				new app.Sealious.SuperContext(),
+				["collections", "B"],
+				"create",
+				{ number }
+			);
+			bs.push(new_b);
+		}
+		for (let b of bs) {
+			for (let i = 1; i <= b.body.number; i++) {
+				await app.run_action(
+					new app.Sealious.SuperContext(),
+					["collections", "A"],
+					"create",
+					{ reference_to_b: b.id, pairity: i % 2 ? "odd" : "even" }
+				);
+			}
+		}
+	}
+
+	it("recreates the cached values if the field has just been added", async () => {
+		await with_stopped_app(async ({ app, dont_clear_database_on_stop }) => {
+			await create_referencing_collections(app, "with_reverse" && false);
+			await app.start();
+			await create_resources(app);
+			dont_clear_database_on_stop();
+		});
+		await with_stopped_app(async ({ base_url, app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			const result = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=1"
+			))[0];
+			assert(result.body.references_in_a);
+			assert.equal(result.body.references_in_a.length, 1);
+			const result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			assert(result2.body.references_in_a);
+			assert.equal(result2.body.references_in_a.length, 2);
+		});
+	});
+
+	it("updates the cached value when a new reference is created", async () => {
+		await with_stopped_app(async ({ app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			await create_resources(app);
+			const result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			assert(result2.body.references_in_a instanceof Array);
+			assert.equal(result2.body.references_in_a.length, 2);
+		});
+	});
+
+	it("updates the cached value when an old reference is deleted", async () => {
+		await with_stopped_app(async ({ app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			await create_resources(app);
+			const result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			const referencing_id = result2.body.references_in_a[0];
+			await rest_api.delete(`/api/v1/collections/A/${referencing_id}`);
+			const new_result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			assert.equal(new_result2.body.references_in_a.length, 1);
+		});
+	});
+
+	it("updates the cached value when an old reference is edited to a new one", async () => {
+		await with_stopped_app(async ({ app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			await create_resources(app);
+			const result1 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=1"
+			))[0];
+			const result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			const referencing_id = result2.body.references_in_a[0];
+
+			await rest_api.patch(`/api/v1/collections/A/${referencing_id}`, {
+				reference_to_b: result1.id,
+			});
+			const new_result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			assert.equal(new_result2.body.references_in_a.length, 1);
+			const new_result1 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=1"
+			))[0];
+			assert.equal(new_result1.body.references_in_a.length, 2);
+		});
+	});
+
+	it("updates the cached value when an old reference is edited to an empty one", async () => {
+		await with_stopped_app(async ({ app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			await create_resources(app);
+			const result1 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=1"
+			))[0];
+			const result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			const referencing_id = result2.body.references_in_a[0];
+
+			await rest_api.patch(`/api/v1/collections/A/${referencing_id}`, {
+				reference_to_b: "",
+			});
+			const new_result2 = (await rest_api.get(
+				"/api/v1/collections/B?filter[number]=2"
+			))[0];
+			assert.equal(new_result2.body.references_in_a.length, 1);
+		});
+	});
+
+	it("allows to filter by a value of the referencing resource", async () => {
+		await with_stopped_app(async ({ app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			await create_resources(app);
+			let results = await rest_api.get(
+				"/api/v1/collections/B?filter[references_in_a][pairity]=non-existant"
+			);
+			assert.equal(results.length, 0);
+			results = await rest_api.get(
+				"/api/v1/collections/B?filter[references_in_a][pairity]=odd"
+			);
+			assert.equal(results.length, 3);
+			results = await rest_api.get(
+				"/api/v1/collections/B?filter[references_in_a][pairity]=even&filter[number]=3"
+			);
+			assert.equal(results.length, 1);
+		});
+	});
+
+	it("allows to display the full body of the referencing resources", async () => {
+		await with_stopped_app(async ({ app, rest_api }) => {
+			await create_referencing_collections(app, "with_reverse" && true);
+			await app.start();
+			await create_resources(app);
+			let results = await rest_api.get(
+				"/api/v1/collections/B?format[references_in_a]=expand"
+			);
+			assert(results[0].body.references_in_a[0].body);
+		});
+	});
+});
diff --git a/lib/app/base-chips/field-types/single_reference.subtest.js b/lib/app/base-chips/field-types/single_reference.subtest.js
--- a/lib/app/base-chips/field-types/single_reference.subtest.js
+++ b/lib/app/base-chips/field-types/single_reference.subtest.js
@@ -2,22 +2,11 @@
 const locreq = require("locreq")(__dirname);
 const axios = require("axios");
 const { create_resource_as } = locreq("test_utils");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("single_reference", () => {
-	let App = null;
-	const port = 8888;
-	const base_url = `http://localhost:${port}/api/v1`;
-	beforeEach(async () => {
-		App = new Sealious.App(
-			{
-				"www-server": { port: 8888 },
-				upload_path: "/dev/null",
-				logger: { level: "emerg" },
-				datastore_mongo: TestApp.ConfigManager.get("datastore_mongo"),
-			},
-			TestApp.manifest
-		);
-		App.createChip(Sealious.Collection, {
+	async function create_referencing_collections(app) {
+		app.createChip(app.Sealious.Collection, {
 			name: "A",
 			fields: [
 				{
@@ -32,69 +21,72 @@
 				},
 			],
 		});
-		App.createChip(Sealious.Collection, {
+		app.createChip(app.Sealious.Collection, {
 			name: "B",
 			fields: [{ name: "number", type: "int" }],
 		});
-		await App.start();
-	});
+	}
 
-	it("should not allow a value that is not an existing id", () => {
-		return axios
-			.post(`${base_url}/collections/A`, {
-				reference_to_b: "non-existing-id",
-			})
-			.then(res => {
-				throw "This should not succeed";
-			})
-			.catch(res =>
-				assert.equal(
-					res.response.data.data.reference_to_b.message,
-					"Nie masz dostępu do danego zasobu z kolekcji B lub on nie istnieje."
-				)
-			);
-	});
-
-	it("should allow a value that exists in B", async () => {
-		const b_id = (await axios.post(`${base_url}/collections/B`, {
-			number: 1,
-		})).data.id;
-		return axios.post(`${base_url}/collections/A`, {
-			reference_to_b: b_id,
-		});
-	});
-
-	it("should not allow a value that exists in B but does not meet the filter criteria", async () => {
-		const b_id = (await axios.post(`${base_url}/collections/B`, {
-			number: 0,
-		})).data.id;
-
-		return axios
-			.post(`${base_url}/collections/A`, {
-				filtered_reference_to_b: b_id,
-			})
-			.then(response => {
-				throw "This should fail";
-			})
-			.catch(error => {
-				assert.equal(
-					error.response.data.data.filtered_reference_to_b.message,
-					"Nie masz dostępu do danego zasobu z kolekcji B lub on nie istnieje."
+	it("should not allow a value that is not an existing id", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			await create_referencing_collections(app);
+			return axios
+				.post(`${base_url}/api/v1/collections/A`, {
+					reference_to_b: "non-existing-id",
+				})
+				.then(res => {
+					throw "This should not succeed";
+				})
+				.catch(res =>
+					assert.equal(
+						res.response.data.data.reference_to_b.message,
+						"Nie masz dostępu do danego zasobu z kolekcji B lub on nie istnieje."
+					)
 				);
+		}));
+
+	it("should allow a value that exists in B", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			create_referencing_collections(app);
+			const b_id = (await axios.post(`${base_url}/api/v1/collections/B`, {
+				number: 1,
+			})).data.id;
+			return axios.post(`${base_url}/api/v1/collections/A`, {
+				reference_to_b: b_id,
 			});
-	});
+		}));
 
-	it("should allow a value that exists in B but does not meet the filter criteria", async () => {
-		const b_id = (await axios.post(`${base_url}/collections/B`, {
-			number: 1,
-		})).data.id;
+	it("should not allow a value that exists in B but does not meet the filter criteria", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			create_referencing_collections(app);
+			const b_id = (await axios.post(`${base_url}/api/v1/collections/B`, {
+				number: 0,
+			})).data.id;
 
-		return axios.post(`${base_url}/collections/A`, {
-			filtered_reference_to_b: b_id,
-		});
-	});
+			return axios
+				.post(`${base_url}/api/v1/collections/A`, {
+					filtered_reference_to_b: b_id,
+				})
+				.then(response => {
+					throw "This should fail";
+				})
+				.catch(error => {
+					assert.equal(
+						error.response.data.data.filtered_reference_to_b.message,
+						"Nie masz dostępu do danego zasobu z kolekcji B lub on nie istnieje."
+					);
+				});
+		}));
+
+	it("should allow a value that exists in B but does not meet the filter criteria", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			create_referencing_collections(app);
+			const b_id = (await axios.post(`${base_url}/api/v1/collections/B`, {
+				number: 1,
+			})).data.id;
 
-	afterEach(async () => {
-		await App.stop();
-	});
+			return axios.post(`${base_url}/api/v1/collections/A`, {
+				filtered_reference_to_b: b_id,
+			});
+		}));
 });
diff --git a/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js b/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js
--- a/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js
+++ b/lib/app/base-chips/special_filters/IsReferencedByResourcesMatching.subtest.js
@@ -5,29 +5,18 @@
 
 const { create_resource_as } = locreq("test_utils");
 const IsReferencedByResourcesMatching = require("./IsReferencedByResourcesMatching");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("IsReferencedByResourcesMatching", () => {
-	let App = null;
-	const port = 8888;
-	const base_url = `http://localhost:${port}/api/v1`;
-	beforeEach(async () => {
-		App = new Sealious.App(
-			{
-				"www-server": { port: 8888 },
-				upload_path: "/dev/null",
-				logger: { level: "emerg" },
-				datastore_mongo: TestApp.ConfigManager.get("datastore_mongo"),
-			},
-			TestApp.manifest
-		);
-
-		const Users = App.ChipManager.get_chip("collection", "users");
+	async function setup(app) {
+		const port = app.ConfigManager.get("www-server.port");
+		const Users = app.ChipManager.get_chip("collection", "users");
 		Users.set_access_strategy({
 			create: "public",
 			retrieve: "public",
 		});
 
-		const UsersRoles = App.createChip(Sealious.Collection, {
+		const UsersRoles = app.createChip(app.Sealious.Collection, {
 			name: "users-roles",
 			fields: [
 				{
@@ -58,8 +47,6 @@
 			}),
 		});
 
-		await App.start();
-
 		const users = [
 			{
 				username: "admin",
@@ -96,25 +83,21 @@
 				port,
 			})
 		);
-	});
+	}
 
 	it("returns only users with role matching `allowed_values`", () =>
-		axios
-			.get(`${base_url}/collections/users/@staff`)
-			.then(resp =>
-				resp.data.forEach(user =>
-					assert(
-						user.body.username === "admin" || user.body.username === "moderator"
-					)
-				)
-			));
-
-	afterEach(async () => {
-		await Promise.all(
-			App.ChipManager.get_all_collections().map(collection_name =>
-				App.Datastore.remove(collection_name, {}, "just_one" && false)
-			)
-		);
-		await App.stop();
-	});
+		with_running_app(async ({ app, base_url }) => {
+			await setup(app);
+			return axios
+				.get(`${base_url}/api/v1/collections/users/@staff`)
+				.then(resp => {
+					assert(resp.data.length > 0);
+					resp.data.forEach(user =>
+						assert(
+							user.body.username === "admin" ||
+								user.body.username === "moderator"
+						)
+					);
+				});
+		}));
 });
diff --git a/lib/app/base-chips/special_filters/matches.subtest.js b/lib/app/base-chips/special_filters/matches.subtest.js
--- a/lib/app/base-chips/special_filters/matches.subtest.js
+++ b/lib/app/base-chips/special_filters/matches.subtest.js
@@ -5,23 +5,12 @@
 
 const { create_resource_as } = locreq("test_utils");
 const matches = require("./matches");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("Matches", () => {
-	let App = null;
-	const port = 8888;
-	const base_url = `http://localhost:${port}/api/v1`;
-	beforeEach(async () => {
-		App = new Sealious.App(
-			{
-				"www-server": { port: 8888 },
-				upload_path: "/dev/null",
-				logger: { level: "emerg" },
-				datastore_mongo: TestApp.ConfigManager.get("datastore_mongo"),
-			},
-			TestApp.manifest
-		);
-
-		App.createChip(Sealious.Collection, {
+	async function setup(app) {
+		const port = app.ConfigManager.get("www-server.port");
+		app.createChip(Sealious.Collection, {
 			name: "numbers",
 			fields: [
 				{
@@ -35,8 +24,6 @@
 			},
 		});
 
-		await App.start();
-
 		const numbers = [-2, -1, 0, 1, 2];
 		await Promise.map(numbers, n =>
 			create_resource_as({
@@ -45,31 +32,30 @@
 				port,
 			})
 		);
-	});
+	}
 
 	it("returns only positive numbers when using @positive filter", () =>
-		axios
-			.get(`${base_url}/collections/numbers/@positive?sort[body.number]=asc`)
-			.then(resp =>
-				assert.deepEqual(resp.data.map(resource => resource.body.number), [
-					1,
-					2,
-				])
-			));
+		with_running_app(async ({ app, base_url }) => {
+			await setup(app);
+			return axios
+				.get(
+					`${base_url}/api/v1/collections/numbers/@positive?sort[body.number]=asc`
+				)
+				.then(resp =>
+					assert.deepEqual(resp.data.map(resource => resource.body.number), [
+						1,
+						2,
+					])
+				);
+		}));
 
 	it("returns empty array when using both @positive and @negative filters", () =>
-		axios
-			.get(`${base_url}/collections/numbers/@positive/@negative`)
-			.then(resp =>
-				assert.deepEqual(resp.data.map(resource => resource.body.number), [])
-			));
-
-	afterEach(async () => {
-		await Promise.all(
-			App.ChipManager.get_all_collections().map(collection_name =>
-				App.Datastore.remove(collection_name, {}, "just_one" && false)
-			)
-		);
-		await App.stop();
-	});
+		with_running_app(async ({ app, base_url }) => {
+			await setup(app);
+			return axios
+				.get(`${base_url}/api/v1/collections/numbers/@positive/@negative`)
+				.then(resp =>
+					assert.deepEqual(resp.data.map(resource => resource.body.number), [])
+				);
+		}));
 });
diff --git a/lib/app/load-base-chips.js b/lib/app/load-base-chips.js
--- a/lib/app/load-base-chips.js
+++ b/lib/app/load-base-chips.js
@@ -45,6 +45,7 @@
 	"value-existing-in-collection",
 	"value-not-existing-in-collection",
 	"secret-token",
+	"reverse-single-reference",
 ]);
 
 BaseChips.set(CalculatedFieldType, ["map-reduce", "aggregate", "custom"]);
diff --git a/lib/app/metadata.js b/lib/app/metadata.js
new file mode 100644
--- /dev/null
+++ b/lib/app/metadata.js
@@ -0,0 +1,24 @@
+const COLLECTION_NAME = "_metadata";
+
+module.exports = app => ({
+	async get(key) {
+		const matches = await app.Datastore.find("COLLECTION_NAME", { key });
+		if (matches.length) {
+			return matches[0].value;
+		} else {
+			undefined;
+		}
+	},
+	async set(key, value) {
+		const matches = await app.Datastore.find("COLLECTION_NAME", { key });
+		if (matches.length) {
+			await app.Datastore.update(
+				"COLLECTION_NAME",
+				{ key: key },
+				{ $set: { value: value } }
+			);
+		} else {
+			await app.Datastore.insert("COLLECTION_NAME", { key, value });
+		}
+	},
+});
diff --git a/lib/chip-types/collection.js b/lib/chip-types/collection.js
--- a/lib/chip-types/collection.js
+++ b/lib/chip-types/collection.js
@@ -73,8 +73,13 @@
 Collection.type_name = "collection";
 
 Collection.pure = {
-	add_field: function(app, field_type, fields, field_declaration) {
-		const field_object = new Field(app, field_declaration, field_type);
+	add_field: function(app, field_type, fields, field_declaration, collection) {
+		const field_object = new Field(
+			app,
+			field_declaration,
+			field_type,
+			collection
+		);
 		const field_name = field_object.name;
 		if (!fields[field_name]) {
 			fields[field_name] = field_object;
@@ -519,7 +524,7 @@
 
 Collection.prototype = {
 	add_field(field_declaration) {
-		return pure.add_field(this.app, this, this.fields, field_declaration);
+		return pure.add_field(this.app, this, this.fields, field_declaration, this);
 	},
 	add_fields(field_declarations_array) {
 		return pure.add_fields(
diff --git a/lib/chip-types/field-type-default-methods.js b/lib/chip-types/field-type-default-methods.js
--- a/lib/chip-types/field-type-default-methods.js
+++ b/lib/chip-types/field-type-default-methods.js
@@ -5,67 +5,73 @@
 const FieldTypeDescription = require("../data-structures/field-type-description.js");
 
 const default_methods = {
-	has_index: function(params){
+	init: function() {
+		return null;
+	},
+	has_index: function(params) {
 		return false;
 	},
-	is_proper_value: function(context, params, new_value, old_value){
+	is_proper_value: function(context, params, new_value, old_value) {
 		return Promise.resolve();
 	},
-	format: function(context, params, decoded_value, format_params){
+	format: function(context, params, decoded_value, format_params) {
 		return decoded_value;
 	},
-	encode: function(context, params, value_in_code){
+	encode: function(context, params, value_in_code) {
 		return value_in_code;
 	},
-	get_description: function(context, params){
+	get_description: function(context, params) {
 		return new FieldTypeDescription(this.name);
 	},
-	decode: function(context, params, value_in_database){
+	decode: function(context, params, value_in_database) {
 		return value_in_database;
 	},
-	filter_to_query: function(context, params, query){
-		return Promise.resolve(this.encode(context, params, query))
-		.then(function(encoded_value){
+	filter_to_query: function(context, params, query) {
+		return Promise.resolve(this.encode(context, params, query)).then(function(
+			encoded_value
+		) {
 			return {
 				$eq: encoded_value,
 			};
 		});
 	},
-	full_text_search_enabled: function(){
+	full_text_search_enabled: function() {
 		return false;
 	},
-	get_aggregation_stages: function(context, params, field_name, query_params){
+	get_aggregation_stages: function(context, params, field_name, query_params) {
 		const self = this;
-		if(!query_params || !query_params.filter) return Promise.resolve([]);
+		if (!query_params || !query_params.filter) return Promise.resolve([]);
 		const expanded_filter = expandHash(query_params.filter);
 		let field_filter = expanded_filter[field_name];
-		if(field_filter && field_filter.length === 1 && field_filter[0] instanceof Array){
+		if (
+			field_filter &&
+			field_filter.length === 1 &&
+			field_filter[0] instanceof Array
+		) {
 			field_filter = field_filter[0]; // to fix an edge case where instead of array of values the array is wrapped within another array
 		}
-		if(!(field_name in expanded_filter)){
+		if (!(field_name in expanded_filter)) {
 			return Promise.resolve([]);
 		}
-		if(field_name in expanded_filter && field_filter === undefined)
-			return Promise.resolve([{$match: {[`body.${field_name}`]: {$exists: false}}}]);
+		if (field_name in expanded_filter && field_filter === undefined)
+			return Promise.resolve([
+				{ $match: { [`body.${field_name}`]: { $exists: false } } },
+			]);
 		let new_filter = null;
-		if(field_filter instanceof Array){
+		if (field_filter instanceof Array) {
 			new_filter = Promise.all(
-				field_filter.map(function(element){
+				field_filter.map(function(element) {
 					return self.encode(context, params, element);
 				})
-			)
-			.then((filters)=> {
-				return {$in: filters};
+			).then(filters => {
+				return { $in: filters };
 			});
-		}else{
+		} else {
 			new_filter = self.filter_to_query(context, params, field_filter);
 		}
-		return new_filter
-			.then(function(filter){
-				return [
-					{$match: {[`body.${field_name}`]: filter}},
-				];
-			});
+		return new_filter.then(function(filter) {
+			return [{ $match: { [`body.${field_name}`]: filter } }];
+		});
 	},
 };
 
diff --git a/lib/chip-types/field.js b/lib/chip-types/field.js
--- a/lib/chip-types/field.js
+++ b/lib/chip-types/field.js
@@ -3,27 +3,29 @@
 const default_methods = require("./field-type-default-methods.js");
 const FieldType = locreq("lib/chip-types/field-type.js");
 
-function Field (app, declaration){
-
+function Field(app, declaration, collection) {
 	this.name = declaration.name;
 	this.declaration = declaration;
 	this.type = new FieldType(app, declaration.type);
 	this.required = declaration.required || false;
 	this.params = declaration.params || {};
+	this.type.init(collection, declaration.name, this.params);
 
 	const self = this;
 
-	for (const method_name in default_methods){
-		this[method_name] = (function(method_name){
-			return function(){
-				const arguments_array = Object.keys(arguments).map((key)=>arguments[key]);
+	for (const method_name in default_methods) {
+		this[method_name] = (function(method_name) {
+			return function() {
+				const arguments_array = Object.keys(arguments).map(
+					key => arguments[key]
+				);
 				arguments_array.splice(1, 0, self.params);
 				return self.type[method_name].apply(self.type, arguments_array);
 			};
 		})(method_name);
 	}
 
-	this.get_specification = function(){
+	this.get_specification = function() {
 		return {
 			name: this.name,
 			type: this.type,
@@ -31,14 +33,17 @@
 		};
 	};
 
-	this.get_aggregation_stages = function(context, query_params){
+	this.get_aggregation_stages = function(context, query_params) {
 		const self = this;
 		return Promise.resolve(
-			self.type.get_aggregation_stages(context, self.params, self.name, query_params)
+			self.type.get_aggregation_stages(
+				context,
+				self.params,
+				self.name,
+				query_params
+			)
 		);
-
 	};
-
 }
 
 module.exports = Field;
diff --git a/lib/email/smtp-mailer.js b/lib/email/smtp-mailer.js
--- a/lib/email/smtp-mailer.js
+++ b/lib/email/smtp-mailer.js
@@ -27,7 +27,7 @@
 				this.mail_config.from_address
 			}>`,
 			to,
-			subject,
+			subject: subject.toString(),
 			text,
 			html,
 			attachments,
diff --git a/lib/email/templates/simple.test.js b/lib/email/templates/simple.test.js
--- a/lib/email/templates/simple.test.js
+++ b/lib/email/templates/simple.test.js
@@ -1,22 +1,21 @@
-const axios = require("axios");
+const locreq = require("locreq")(__dirname);
 const assert = require("assert");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("simpleTemplate", () => {
-	let smtp_api = null;
-	before(() => (smtp_api = TestApp.ConfigManager.get("tests.smtp_api_url")));
-	it("sends an email", async () => {
-		await axios.delete(`${smtp_api}/messages`);
-		const message = await TestApp.EmailTemplates.Simple(TestApp, {
-			to: "test@example.com",
-			subject: "Congratulations!",
-			text: "Enlarge your 'seal' with herbal supplements",
-		});
-		await message.send(TestApp);
-		const messages = (await axios.get(`${smtp_api}/messages`)).data;
-		assert.equal(messages.length, 1);
-		assert.equal(
-			messages[0].sender,
-			`<${TestApp.ConfigManager.get("email.from_address")}>`
-		);
-	});
+	it("sends an email", async () =>
+		with_running_app(async ({ app, mail_api }) => {
+			const message = await app.EmailTemplates.Simple(app, {
+				to: "test@example.com",
+				subject: "Congratulations!",
+				text: "Enlarge your 'seal' with herbal supplements",
+			});
+			await message.send(app);
+			const messages = await mail_api.get_messages();
+			assert.equal(messages.length, 1);
+			assert.equal(
+				messages[0].sender,
+				`<${app.ConfigManager.get("email.from_address")}>`
+			);
+		}));
 });
diff --git a/lib/http/routes/confirm-password-reset.test.js b/lib/http/routes/confirm-password-reset.test.js
--- a/lib/http/routes/confirm-password-reset.test.js
+++ b/lib/http/routes/confirm-password-reset.test.js
@@ -1,11 +1,13 @@
+const locreq = require("locreq")(__dirname);
 const axios = require("axios");
+const assert = require("assert");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("confirm-password-reset", () => {
-	it("displays an html form", async () => {
-		const response = await axios.get(
-			`${
-				TestApp.manifest.base_url
-			}/confirm-password-reset?token=kupcia&email=dupcia`
-		);
-	});
+	it("displays an html form", async () =>
+		with_running_app(async ({ app, base_url }) => {
+			const response = await axios.get(
+				`${base_url}/confirm-password-reset?token=kupcia&email=dupcia`
+			);
+		}));
 });
diff --git a/lib/http/routes/finalize-password-reset.test.js b/lib/http/routes/finalize-password-reset.test.js
--- a/lib/http/routes/finalize-password-reset.test.js
+++ b/lib/http/routes/finalize-password-reset.test.js
@@ -3,28 +3,10 @@
 const axios = require("axios");
 const tough = require("tough-cookie");
 const { promise_timeout, assert_throws_async } = locreq("test_utils");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("finalize password reset", () => {
-	let app = null;
-	const port = 8888;
-	const base_url = `http://localhost:${port}`;
-	let smtp_api = null;
-	before(() => (smtp_api = TestApp.ConfigManager.get("tests.smtp_api_url")));
-
-	beforeEach(async () => {
-		app = new Sealious.App(
-			{
-				"www-server": { port: 8888 },
-				upload_path: "/dev/null",
-				logger: { level: "emerg" },
-				core: { environment: "production" },
-				smtp: TestApp.ConfigManager.get("smtp"),
-				datastore_mongo: TestApp.ConfigManager.get("datastore_mongo"),
-			},
-			TestApp.manifest
-		);
-
-		await app.start();
+	async function create_a_user(app) {
 		await app.run_action(
 			new app.Sealious.SuperContext(),
 			["collections", "users"],
@@ -35,54 +17,55 @@
 				password: "password",
 			}
 		);
-		await axios.delete(`${smtp_api}/messages`);
-	});
+	}
 
-	it("allows to change a password (entire flow)", async () => {
-		const cookieJar = new tough.CookieJar();
-		const options = {
-			jar: cookieJar,
-			withCredentials: true,
-		};
+	it("allows to change a password (entire flow)", async () =>
+		with_running_app(async ({ app, base_url, mail_api }) => {
+			await create_a_user(app);
+			const cookieJar = new tough.CookieJar();
+			const options = {
+				jar: cookieJar,
+				withCredentials: true,
+			};
 
-		await axios.post(
-			`${base_url}/api/v1/sessions`,
-			{ username: "user", password: "password" },
-			options
-		);
-		await axios.delete(`${base_url}/api/v1/sessions/current`, options);
-		await axios.post(`${base_url}/api/v1/collections/password-reset-intents`, {
-			email: "user@example.com",
-		});
+			await axios.post(
+				`${base_url}/api/v1/sessions`,
+				{ username: "user", password: "password" },
+				options
+			);
+			await axios.delete(`${base_url}/api/v1/sessions/current`, options);
+			await axios.post(
+				`${base_url}/api/v1/collections/password-reset-intents`,
+				{
+					email: "user@example.com",
+				}
+			);
 
-		const message = (await axios.get(`${smtp_api}/messages/1.html`)).data;
-		const token = message.match(/token=([^?&]+)/)[1];
-		await axios.post(`${base_url}/finalize-password-reset`, {
-			email: "user@example.com",
-			token,
-			password: "new-password",
-		});
-		await axios.post(
-			`${base_url}/api/v1/sessions`,
-			{ username: "user", password: "new-password" },
-			options
-		);
+			const message_metadata = (await mail_api.get_messages()).filter(
+				message => message.recipients[0] == "<user@example.com>"
+			)[0];
+			assert(message_metadata.subject);
+
+			const message = await mail_api.get_message_by_id(message_metadata.id);
 
-		assert_throws_async(async () => {
+			const token = message.match(/token=([^?&]+)/)[1];
 			await axios.post(`${base_url}/finalize-password-reset`, {
 				email: "user@example.com",
 				token,
-				password: "using the same token twice hehehehhee",
+				password: "new-password",
 			});
-		});
-	});
+			await axios.post(
+				`${base_url}/api/v1/sessions`,
+				{ username: "user", password: "new-password" },
+				options
+			);
 
-	afterEach(async () => {
-		await Promise.all(
-			app.ChipManager.get_all_collections().map(collection_name =>
-				app.Datastore.remove(collection_name, {}, "just_one" && false)
-			)
-		);
-		await app.stop();
-	});
+			assert_throws_async(async () => {
+				await axios.post(`${base_url}/finalize-password-reset`, {
+					email: "user@example.com",
+					token,
+					password: "using the same token twice hehehehhee",
+				});
+			});
+		}));
 });
diff --git a/lib/http/routes/finalize-registration-intent.test.js b/lib/http/routes/finalize-registration-intent.test.js
--- a/lib/http/routes/finalize-registration-intent.test.js
+++ b/lib/http/routes/finalize-registration-intent.test.js
@@ -3,67 +3,37 @@
 const axios = require("axios");
 const tough = require("tough-cookie");
 const { promise_timeout, assert_throws_async } = locreq("test_utils");
+const { with_running_app } = locreq("test_utils/with-test-app.js");
 
 describe("finalize registration", () => {
-	let app = null;
-	const port = 8888;
-	const base_url = `http://localhost:${port}`;
-	let smtp_api = null;
-	before(() => (smtp_api = TestApp.ConfigManager.get("tests.smtp_api_url")));
-
-	beforeEach(async () => {
-		app = new Sealious.App(
-			{
-				"www-server": { port: 8888 },
-				upload_path: "/dev/null",
-				logger: { level: "error" },
-				core: { environment: "production" },
-				smtp: TestApp.ConfigManager.get("smtp"),
-				datastore_mongo: TestApp.ConfigManager.get("datastore_mongo"),
-			},
-			TestApp.manifest
-		);
-
-		await app.start();
-		await axios.delete(`${smtp_api}/messages`);
-	});
-
-	it("allows to register an account (entire flow)", async () => {
-		const cookieJar = new tough.CookieJar();
-		const options = {
-			jar: cookieJar,
-			withCredentials: true,
-		};
-
-		await axios.post(
-			`${base_url}/api/v1/collections/registration-intents`,
-			{ email: "user@example.com" },
-			options
-		);
-
-		const message = (await axios.get(`${smtp_api}/messages/1.html`)).data;
-		const token = message.match(/token=([^?&]+)/)[1];
-
-		await axios.post(`${base_url}/finalize-registration-intent`, {
-			email: "user@example.com",
-			token,
-			password: "password",
-			username: "user",
-		});
-
-		await axios.post(
-			`${base_url}/api/v1/sessions`,
-			{ username: "user", password: "password" },
-			options
-		);
-	});
-
-	afterEach(async () => {
-		await Promise.all(
-			app.ChipManager.get_all_collections().map(collection_name =>
-				app.Datastore.remove(collection_name, {}, "just_one" && false)
-			)
-		);
-		await app.stop();
-	});
+	it("allows to register an account (entire flow)", async () =>
+		with_running_app(async ({ app, base_url, mail_api }) => {
+			const cookieJar = new tough.CookieJar();
+			const options = {
+				jar: cookieJar,
+				withCredentials: true,
+			};
+
+			await axios.post(
+				`${base_url}/api/v1/collections/registration-intents`,
+				{ email: "user@example.com" },
+				options
+			);
+
+			const message = await mail_api.get_message_by_id(1);
+			const token = message.match(/token=([^?&]+)/)[1];
+
+			await axios.post(`${base_url}/finalize-registration-intent`, {
+				email: "user@example.com",
+				token,
+				password: "password",
+				username: "user",
+			});
+
+			await axios.post(
+				`${base_url}/api/v1/sessions`,
+				{ username: "user", password: "password" },
+				options
+			);
+		}));
 });
diff --git a/setup-test.js b/setup-test.js
--- a/setup-test.js
+++ b/setup-test.js
@@ -5,47 +5,4 @@
 const axiosCookieJarSupport = require("@3846masa/axios-cookiejar-support");
 axiosCookieJarSupport(axios);
 
-before(async () => {
-	global.TestApp = new Sealious.App(
-		{
-			upload_path: "/tmp",
-			datastore_mongo: { host: "db", password: "sealious-test" },
-			smtp: {
-				host: "mailcatcher",
-				port: 1025,
-				user: "any",
-				password: "any",
-			},
-			email: {
-				from_name: "Sealious test app",
-				from_address: "sealious@example.com",
-			},
-			core: { environment: "production" },
-			app: { version: "0.0.0-test" },
-			logger: { level: "emerg" },
-			tests: {
-				//non-standard, just for testing
-				smtp_api_url: "http://mailcatcher:1080",
-			},
-		},
-		{
-			name: "testing app",
-			logo: locreq.resolve("lib/assets/logo.png"),
-			default_language: "pl",
-			version: "0.0.0-test",
-			base_url: "http://localhost:8080",
-			colors: {
-				primary: "#4d394b",
-			},
-		}
-	);
-	global.Sealious = Sealious;
-	return TestApp.start().catch(error => {
-		console.error(error);
-		process.exit(1);
-	});
-});
-
-after(async () => {
-	await TestApp.stop();
-});
+global.Sealious = Sealious;
diff --git a/test_utils/with-test-app.js b/test_utils/with-test-app.js
new file mode 100644
--- /dev/null
+++ b/test_utils/with-test-app.js
@@ -0,0 +1,96 @@
+const locreq = require("locreq")(__dirname);
+const axios = require("axios");
+
+module.exports = {
+	with_stopped_app: with_test_app.bind(global, "auto_start" && false),
+	with_running_app: with_test_app.bind(global, "auto_start" && true),
+};
+
+async function with_test_app(auto_start, fn) {
+	let app = null;
+	const port = 8888;
+	const base_url = `http://localhost:${port}`;
+	const smtp_api_url = "http://mailcatcher:1080";
+
+	app = new Sealious.App(
+		{
+			upload_path: "/tmp",
+			datastore_mongo: { host: "db", password: "sealious-test" },
+			smtp: {
+				host: "mailcatcher",
+				port: 1025,
+				user: "any",
+				password: "any",
+			},
+			email: {
+				from_name: "Sealious test app",
+				from_address: "sealious@example.com",
+			},
+			core: { environment: "production" },
+			app: { version: "0.0.0-test" },
+			logger: { level: "emerg" },
+			"www-server": {
+				port,
+			},
+		},
+		{
+			name: "testing app",
+			logo: locreq.resolve("lib/assets/logo.png"),
+			default_language: "pl",
+			version: "0.0.0-test",
+			base_url,
+			colors: {
+				primary: "#4d394b",
+			},
+			admin_email: "admin@example.com",
+		}
+	);
+
+	let clear_database_on_stop = true;
+
+	app.on("stop", async () => {
+		if (clear_database_on_stop) {
+			return Promise.all(
+				app.ChipManager.get_all_collections().map(collection_name =>
+					app.Datastore.remove(collection_name, {}, "just_one" && false)
+				)
+			);
+		}
+	});
+
+	if (auto_start) {
+		await app.start();
+	}
+
+	try {
+		await axios.delete(`${smtp_api_url}/messages`);
+
+		await fn({
+			app,
+			base_url,
+			smtp_api_url,
+			mail_api: {
+				get_messages: async () =>
+					(await axios.get(`${smtp_api_url}/messages`)).data,
+				get_message_by_id: async id =>
+					(await axios.get(`${smtp_api_url}/messages/${id}.html`)).data,
+			},
+			dont_clear_database_on_stop: () => (clear_database_on_stop = false),
+			rest_api: {
+				get: async url => (await axios.get(`${base_url}${url}`)).data,
+				delete: async url => (await axios.delete(`${base_url}${url}`)).data,
+				patch: async (url, data) =>
+					(await axios.patch(`${base_url}${url}`, data)).data,
+			},
+		});
+
+		if (app.status !== "stopped") {
+			await app.stop();
+		}
+	} catch (e) {
+		if (app.status !== "stopped") {
+			await app.stop();
+		}
+		throw e;
+	}
+}