diff --git a/changelogs/unreleased/fully-temporary-spaces.md b/changelogs/unreleased/fully-temporary-spaces.md
new file mode 100644
index 0000000000000000000000000000000000000000..c92aa212d90026b68cdc5a6d67862d65abbc7702
--- /dev/null
+++ b/changelogs/unreleased/fully-temporary-spaces.md
@@ -0,0 +1,6 @@
+## feature/space
+
+* Introduces the fully temporary space type. It is the same as data-temporary
+  but also has temporary metadata. Temporary spaces can now be created in
+  read_only mode, they disappear after server restart and don't exist on
+  replicas (gh-8323).
diff --git a/src/box/alter.cc b/src/box/alter.cc
index c065a4a210aaaac7ea30f42c68ebc9a77b30e0ce..7811fd278776f8e2f4b59cab4e96e28c4b099f0e 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -531,6 +531,11 @@ space_def_new_from_tuple(struct tuple *tuple, uint32_t errcode,
 			 "local space can't be synchronous");
 		return NULL;
 	}
+	if (space_opts_is_temporary(&opts) && opts.constraint_count > 0) {
+		diag_set(ClientError, ER_UNSUPPORTED, "temporary space",
+			 "constraints");
+		return NULL;
+	}
 	struct space_def *def =
 		space_def_new(id, uid, exact_field_count, name, name_len,
 			      engine_name, engine_name_len, &opts, fields,
@@ -1932,6 +1937,11 @@ update_view_references(struct Select *select, int update_value)
 			diag_set(ClientError, ER_NO_SUCH_SPACE, space_name);
 			goto error;
 		}
+		if (space_is_temporary(space)) {
+			diag_set(ClientError, ER_UNSUPPORTED,
+				 "CREATE VIEW", "temporary spaces");
+			goto error;
+		}
 	}
 	/* Secondly do the job. */
 	for (int i = 0; i < from_tables_count; ++i) {
@@ -2112,6 +2122,27 @@ space_check_alter(struct space *old_space, struct space_def *new_space_def)
 	return 0;
 }
 
+/*
+ * box_process1() bypasses the read-only check for the _space system space
+ * because there it's not yet known if the related space is temporary. Perform
+ * the check here if the space isn't temporary and the statement was issued by
+ * this replica.
+ */
+static int
+filter_temporary_ddl_stmt(struct txn *txn, const struct space_def *def)
+{
+	if (def == NULL)
+		return 0;
+	struct txn_stmt *stmt = txn_current_stmt(txn);
+	if (space_opts_is_temporary(&def->opts)) {
+		txn_stmt_mark_as_temporary(txn, stmt);
+		return 0;
+	}
+	if (stmt->row->replica_id == 0 && recovery_state != INITIAL_RECOVERY)
+		return box_check_writable();
+	return 0;
+}
+
 /**
  * A trigger which is invoked on replace in a data dictionary
  * space _space.
@@ -2195,6 +2226,9 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 			ER_CREATE_SPACE : ER_ALTER_SPACE;
 		def = space_def_new_from_tuple(new_tuple, errcode, region);
 	}
+	if (filter_temporary_ddl_stmt(txn, old_space != NULL ?
+				      old_space->def : def) != 0)
+		return -1;
 	if (new_tuple != NULL && old_space == NULL) { /* INSERT */
 		if (def == NULL)
 			return -1;
@@ -2380,6 +2414,13 @@ on_replace_dd_space(struct trigger * /* trigger */, void *event)
 				  "replication group is immutable");
 			return -1;
 		}
+		if (space_is_temporary(old_space) !=
+		     space_opts_is_temporary(&def->opts)) {
+			diag_set(ClientError, ER_ALTER_SPACE,
+				 old_space->def->name,
+				 "temporariness cannot change");
+			return -1;
+		}
 		if (def->opts.is_view != old_space->def->opts.is_view) {
 			diag_set(ClientError, ER_ALTER_SPACE,
 				  space_name(old_space),
@@ -2518,6 +2559,8 @@ on_replace_dd_index(struct trigger * /* trigger */, void *event)
 	struct space *old_space = space_cache_find(id);
 	if (old_space == NULL)
 		return -1;
+	if (filter_temporary_ddl_stmt(txn, old_space->def) != 0)
+		return -1;
 	if (old_space->def->opts.is_view) {
 		diag_set(ClientError, ER_ALTER_SPACE, space_name(old_space),
 			  "can not add index on a view");
@@ -2778,21 +2821,28 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event)
 	struct txn_stmt *stmt = txn_current_stmt(txn);
 	struct tuple *new_tuple = stmt->new_tuple;
 
-	if (new_tuple == NULL) {
-		/* Space drop - nothing to do. */
-		return 0;
-	}
 	if (recovery_state == INITIAL_RECOVERY) {
 		/* Space creation during initial recovery - nothing to do. */
 		return 0;
 	}
 
+	struct tuple *any_tuple = new_tuple;
+	if (any_tuple == NULL)
+		any_tuple = stmt->old_tuple;
 	uint32_t space_id;
-	if (tuple_field_u32(new_tuple, BOX_TRUNCATE_FIELD_SPACE_ID, &space_id) != 0)
+	if (tuple_field_u32(any_tuple, BOX_TRUNCATE_FIELD_SPACE_ID,
+			    &space_id) != 0)
 		return -1;
 	struct space *old_space = space_cache_find(space_id);
 	if (old_space == NULL)
 		return -1;
+	if (space_is_temporary(old_space))
+		txn_stmt_mark_as_temporary(txn, stmt);
+
+	if (new_tuple == NULL) {
+		/* Space drop - nothing else to do. */
+		return 0;
+	}
 
 	/*
 	 * box_process1() bypasses the read-only check for the _truncate system
@@ -2838,9 +2888,11 @@ on_replace_dd_truncate(struct trigger * /* trigger */, void *event)
 	/*
 	 * Modify the WAL header to prohibit
 	 * replication of local & data-temporary
-	 * spaces truncation.
+	 * spaces truncation
+	 * unless it's a temporary space
+	 * in which case the header doesn't exist.
 	 */
-	if (is_temp) {
+	if (is_temp && !space_is_temporary(old_space)) {
 		stmt->row->group_id = GROUP_LOCAL;
 		/*
 		 * The trigger is invoked after txn->n_local_rows
@@ -3874,6 +3926,11 @@ priv_def_check(struct priv_def *priv, enum priv_type priv_type)
 				  grantor->def->name);
 			return -1;
 		}
+		if (space_is_temporary(space)) {
+			diag_set(ClientError, ER_UNSUPPORTED,
+				 "temporary space", "privileges");
+			return -1;
+		}
 		break;
 	}
 	case SC_FUNCTION:
@@ -4782,6 +4839,11 @@ on_replace_dd_space_sequence(struct trigger * /* trigger */, void *event)
 	struct space *space = space_cache_find(space_id);
 	if (space == NULL)
 		return -1;
+	if (space_is_temporary(space)) {
+		diag_set(ClientError, ER_SQL_EXECUTE,
+			 "sequences are not supported for temporary spaces");
+		return -1;
+	}
 	struct sequence *seq = sequence_by_id(sequence_id);
 	if (seq == NULL) {
 		diag_set(ClientError, ER_NO_SUCH_SEQUENCE, int2str(sequence_id));
@@ -5023,6 +5085,13 @@ on_replace_dd_trigger(struct trigger * /* trigger */, void *event)
 				  "resolved on AST building from SQL");
 			return -1;
 		}
+		struct space *space = space_cache_find(space_id);
+		if (space != NULL && space_is_temporary(space)) {
+			diag_set(ClientError, ER_SQL_EXECUTE,
+				 "triggers are not supported for "
+				 "temporary spaces");
+			return -1;
+		}
 
 		struct sql_trigger *old_trigger;
 		if (sql_trigger_replace(trigger_name,
@@ -5077,6 +5146,11 @@ on_replace_dd_func_index(struct trigger *trigger, void *event)
 		space = space_cache_find(space_id);
 		if (space == NULL)
 			return -1;
+		if (space_is_temporary(space)) {
+			diag_set(ClientError, ER_UNSUPPORTED,
+				 "temporary space", "functional indexes");
+			return -1;
+		}
 		index = index_find(space, index_id);
 		if (index == NULL)
 			return -1;
diff --git a/src/box/box.cc b/src/box/box.cc
index b5614051382dfa2686fbdfe71eebc643426e2ad7..588b7d29f4e8399e5f6c6a575d84bb447942e7a6 100644
--- a/src/box/box.cc
+++ b/src/box/box.cc
@@ -3417,11 +3417,16 @@ box_process1(struct request *request, box_tuple_t **result)
 		return -1;
 	/*
 	 * Allow to write to data-temporary and local spaces in the read-only
-	 * mode. To handle space truncation, we postpone the read-only check for
-	 * the _truncate system space till the on_replace trigger is called,
-	 * when we know which space is truncated.
+	 * mode. To handle space truncation and/or ddl operations on temporary
+	 * spaces, we postpone the read-only check for the _truncate, _space &
+	 * _index system spaces till the on_replace trigger is called, when
+	 * we know which spaces are concerned.
 	 */
-	if (space_id(space) != BOX_TRUNCATE_ID &&
+	uint32_t id = space_id(space);
+	if (is_ro_summary &&
+	    id != BOX_TRUNCATE_ID &&
+	    id != BOX_SPACE_ID &&
+	    id != BOX_INDEX_ID &&
 	    !space_is_data_temporary(space) &&
 	    !space_is_local(space) &&
 	    box_check_writable() != 0)
@@ -5477,37 +5482,50 @@ box_read_ffi_enable(void)
 }
 
 int
-box_generate_space_id(uint32_t *new_space_id)
+box_generate_space_id(uint32_t *new_space_id, bool is_temporary)
 {
 	assert(new_space_id != NULL);
-	assert(mp_sizeof_array(0) == 1);
-	char empty_key[1];
-	char *empty_key_end = mp_encode_array(empty_key, 0);
-	struct tuple *res = NULL;
+	uint32_t id_range_begin = !is_temporary ?
+		BOX_SYSTEM_ID_MAX + 1 : BOX_SPACE_ID_TEMPORARY_MIN;
+	uint32_t id_range_end = !is_temporary ?
+		(uint32_t)BOX_SPACE_ID_TEMPORARY_MIN :
+		(uint32_t)BOX_SPACE_MAX + 1;
+	char key_buf[16];
+	char *key_end = key_buf;
+	key_end = mp_encode_array(key_end, 1);
+	key_end = mp_encode_uint(key_end, id_range_end);
 	struct credentials *orig_credentials = effective_user();
 	fiber_set_user(fiber(), &admin_credentials);
-	int rc = box_index_max(BOX_SPACE_ID, 0, empty_key, empty_key_end,
-			       &res);
-	fiber_set_user(fiber(), orig_credentials);
+	auto guard = make_scoped_guard([=] {
+		fiber_set_user(fiber(), orig_credentials);
+	});
+	box_iterator_t *it = box_index_iterator(BOX_SPACE_ID, 0, ITER_LT,
+						key_buf, key_end);
+	if (it == NULL)
+		return -1;
+	struct tuple *res = NULL;
+	int rc = box_iterator_next(it, &res);
+	box_iterator_free(it);
 	if (rc != 0)
 		return -1;
+	assert(res != NULL);
 	uint32_t max_id = 0;
-	if (res != NULL && tuple_field_u32(res, 0, &max_id) != 0)
-		return -1;
-	if (max_id > BOX_SPACE_MAX || max_id < BOX_SYSTEM_ID_MAX)
-		max_id = BOX_SYSTEM_ID_MAX;
+	rc = tuple_field_u32(res, 0, &max_id);
+	assert(rc == 0);
+	if (max_id < id_range_begin)
+		max_id = id_range_begin - 1;
 	*new_space_id = space_cache_find_next_unused_id(max_id);
 	/* Try again if overflowed. */
-	if (*new_space_id > BOX_SPACE_MAX) {
+	if (*new_space_id >= id_range_end) {
 		*new_space_id =
-			space_cache_find_next_unused_id(BOX_SYSTEM_ID_MAX);
+			space_cache_find_next_unused_id(id_range_begin - 1);
 		/*
 		 * The second overflow means all ids are occupied.
 		 * This situation cannot happen in real world with limited
 		 * memory, and its pretty hard to test it, so let's just panic
 		 * if we've run out of ids.
 		 */
-		if (*new_space_id > BOX_SPACE_MAX)
+		if (*new_space_id >= id_range_end)
 			panic("Space id limit is reached");
 	}
 	return 0;
diff --git a/src/box/box.h b/src/box/box.h
index af95eedc0c94489fb884de826b1150a9d496f1f4..5f2a70ad96e0460b6b21e81cf7b4032f1c2fea77 100644
--- a/src/box/box.h
+++ b/src/box/box.h
@@ -759,7 +759,7 @@ boxk(int type, uint32_t space_id, const char *format, ...);
 
 /** Generate unique id for non-system space. */
 int
-box_generate_space_id(uint32_t *new_space_id);
+box_generate_space_id(uint32_t *new_space_id, bool is_temporary);
 
 /**
  * Broadcast the identification of the instance
diff --git a/src/box/lua/misc.cc b/src/box/lua/misc.cc
index f0676873afe68bc2e7f932ea4a10a13b25ad560c..6da577f54e84c1b587475bcf8c2ae6c9985fc41a 100644
--- a/src/box/lua/misc.cc
+++ b/src/box/lua/misc.cc
@@ -225,8 +225,11 @@ port_msgpack_dump_lua(struct port *base, struct lua_State *L, bool is_flat)
 static int
 lbox_generate_space_id(lua_State *L)
 {
+	assert(lua_gettop(L) >= 1);
+	assert(lua_isboolean(L, 1) == 1);
+	bool is_temporary = lua_toboolean(L, 1) != 0;
 	uint32_t ret = 0;
-	if (box_generate_space_id(&ret) != 0)
+	if (box_generate_space_id(&ret, is_temporary) != 0)
 		return luaT_error(L);
 	lua_pushnumber(L, ret);
 	return 1;
diff --git a/src/box/lua/schema.lua b/src/box/lua/schema.lua
index 2097128f55d03a3d9301492bba736db6235eed51..7a978636dad6eafb575b313fb123f1a861b5d957 100644
--- a/src/box/lua/schema.lua
+++ b/src/box/lua/schema.lua
@@ -677,6 +677,7 @@ end
 local space_types = {
     'normal',
     'data-temporary',
+    'temporary',
 }
 local function check_space_type(space_type)
     if space_type == nil then
@@ -737,7 +738,7 @@ box.schema.space.create = function(name, options)
     end
     local id = options.id
     if not id then
-        id = internal.generate_space_id()
+        id = internal.generate_space_id(options.type == 'temporary')
     end
     local uid = session.euid()
     if options.user then
@@ -807,10 +808,15 @@ box.schema.space.drop = function(space_id, space_name, opts)
     local _truncate = box.space[box.schema.TRUNCATE_ID]
     local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
     local _func_index = box.space[box.schema.FUNC_INDEX_ID]
-    local sequence_tuple = _space_sequence:delete{space_id}
-    if sequence_tuple ~= nil and sequence_tuple.is_generated == true then
-        -- Delete automatically generated sequence.
-        box.schema.sequence.drop(sequence_tuple.sequence_id)
+    -- This is needed to support dropping temporary spaces
+    -- in read-only mode, because sequences aren't supported for them yet
+    -- and therefore such requests aren't allowed in read-only mode.
+    if _space_sequence:get(space_id) ~= nil then
+        local sequence_tuple = _space_sequence:delete{space_id}
+        if sequence_tuple.is_generated == true then
+            -- Delete automatically generated sequence.
+            box.schema.sequence.drop(sequence_tuple.sequence_id)
+        end
     end
     for _, t in _trigger.index.space_id:pairs({space_id}) do
         _trigger:delete({t.name})
@@ -824,7 +830,15 @@ box.schema.space.drop = function(space_id, space_name, opts)
         _index:delete{v.id, v.iid}
     end
     revoke_object_privs('space', space_id)
-    _truncate:delete{space_id}
+    -- Deleting from _truncate currently adds a delete entry into WAL even
+    -- if the corresponding space was never truncated. This is a problem for
+    -- temporary spaces, because in such situations it's impossible to
+    -- filter out such entries from the within on_replace trigger which
+    -- basically results in temporary space's metadata getting into WAL
+    -- which breaks some invariants.
+    if _truncate:get{space_id} ~= nil then
+        _truncate:delete{space_id}
+    end
     if _space:delete{space_id} == nil then
         if space_name == nil then
             space_name = '#'..tostring(space_id)
@@ -1569,10 +1583,15 @@ box.schema.index.drop = function(space_id, index_id)
     check_param(index_id, 'index_id', 'number')
     if index_id == 0 then
         local _space_sequence = box.space[box.schema.SPACE_SEQUENCE_ID]
-        local sequence_tuple = _space_sequence:delete{space_id}
-        if sequence_tuple ~= nil and sequence_tuple.is_generated == true then
-            -- Delete automatically generated sequence.
-            box.schema.sequence.drop(sequence_tuple.sequence_id)
+        -- This is needed to support dropping temporary spaces
+        -- in read-only mode, because sequences aren't supported for them yet
+        -- and therefore such requests aren't allowed in read-only mode.
+        if _space_sequence:get(space_id) ~= nil then
+            local sequence_tuple = _space_sequence:delete{space_id}
+            if sequence_tuple.is_generated == true then
+                -- Delete automatically generated sequence.
+                box.schema.sequence.drop(sequence_tuple.sequence_id)
+            end
         end
     end
     local _index = box.space[box.schema.INDEX_ID]
diff --git a/src/box/lua/space.cc b/src/box/lua/space.cc
index 953077bde704c312270cde63f007f042bb53fbec..ad554462f41789d20b863f48f6c4d9f393b8517e 100644
--- a/src/box/lua/space.cc
+++ b/src/box/lua/space.cc
@@ -780,6 +780,8 @@ box_lua_space_init(struct lua_State *L)
 	lua_setfield(L, -2, "REPLICA_MAX");
 	lua_pushnumber(L, SQL_BIND_PARAMETER_MAX);
 	lua_setfield(L, -2, "SQL_BIND_PARAMETER_MAX");
+	lua_pushnumber(L, BOX_SPACE_ID_TEMPORARY_MIN);
+	lua_setfield(L, -2, "SPACE_ID_TEMPORARY_MIN");
 	lua_pop(L, 2); /* box, schema */
 
 	static const struct luaL_Reg space_internal_lib[] = {
diff --git a/src/box/memtx_engine.cc b/src/box/memtx_engine.cc
index c57fe518d622e3fa14362c75f46ebbddf7ae1ae6..8d9ded8789243d8a8b77761f1e7b94680df92a38 100644
--- a/src/box/memtx_engine.cc
+++ b/src/box/memtx_engine.cc
@@ -808,6 +808,46 @@ primary_index_filter(struct space *space, struct index *index, void *arg)
 	return index->def->iid == 0;
 }
 
+/*
+ * Return true if tuple @a data represents temporary space's metadata.
+ * @a space_id is used to determine the tuple's format.
+ */
+static bool
+is_tuple_temporary(const char *data, uint32_t space_id,
+		   struct mh_i32_t *temp_space_ids)
+{
+	static_assert(BOX_SPACE_ID < BOX_INDEX_ID &&
+		      BOX_SPACE_ID < BOX_TRUNCATE_ID,
+		      "in this function temporary space ids are collected "
+		      "while processing tuples of _space and then this info is "
+		      "used when going over _index & _truncate");
+	switch (space_id) {
+	case BOX_SPACE_ID: {
+		uint32_t space_id = BOX_ID_NIL;
+		bool t = space_def_tuple_is_temporary(data, &space_id);
+		if (t)
+			mh_i32_put(temp_space_ids, &space_id,
+				   NULL, NULL);
+		return t;
+	}
+	case BOX_INDEX_ID:
+	case BOX_TRUNCATE_ID: {
+		static_assert(BOX_INDEX_FIELD_SPACE_ID == 0 &&
+			      BOX_TRUNCATE_FIELD_SPACE_ID == 0,
+			      "the following code assumes this is true");
+		uint32_t field_count = mp_decode_array(&data);
+		if (field_count < 1 || mp_typeof(*data) != MP_UINT)
+			return false;
+		uint32_t space_id = mp_decode_uint(&data);
+		mh_int_t pos = mh_i32_find(temp_space_ids, space_id,
+					   NULL);
+		return pos != mh_end(temp_space_ids);
+	}
+	default:
+		return false;
+	}
+}
+
 static struct checkpoint *
 checkpoint_new(const char *snap_dirname, uint64_t snap_io_rate_limit)
 {
@@ -993,6 +1033,8 @@ checkpoint_f(va_list ap)
 		return -1;
 
 	bool is_synchro_written = false;
+	struct mh_i32_t *temp_space_ids = mh_i32_new();
+
 	say_info("saving snapshot `%s'", snap.filename);
 	ERROR_INJECT_SLEEP(ERRINJ_SNAP_WRITE_DELAY);
 	ERROR_INJECT(ERRINJ_SNAP_SKIP_ALL_ROWS, goto done);
@@ -1033,6 +1075,10 @@ checkpoint_f(va_list ap)
 			rc = index_read_view_iterator_next_raw(&it, &result);
 			if (rc != 0 || result.data == NULL)
 				break;
+			if (is_tuple_temporary(result.data,
+					       space_rv->id,
+					       temp_space_ids))
+				continue;
 			rc = checkpoint_write_tuple(&snap, space_rv->id,
 						    space_rv->group_id,
 						    result.data, result.size);
@@ -1043,6 +1089,7 @@ checkpoint_f(va_list ap)
 		if (rc != 0)
 			break;
 	}
+	mh_i32_delete(temp_space_ids);
 	if (rc != 0)
 		goto fail;
 	ERROR_INJECT(ERRINJ_SNAP_WRITE_CORRUPTED_INSERT_ROW, {
@@ -1356,6 +1403,7 @@ memtx_join_f(va_list ap)
 {
 	int rc = 0;
 	struct memtx_join_ctx *ctx = va_arg(ap, struct memtx_join_ctx *);
+	struct mh_i32_t *temp_space_ids = mh_i32_new();
 	struct space_read_view *space_rv;
 	read_view_foreach_space(space_rv, &ctx->rv) {
 		FiberGCChecker gc_check;
@@ -1374,6 +1422,10 @@ memtx_join_f(va_list ap)
 			rc = index_read_view_iterator_next_raw(&it, &result);
 			if (rc != 0 || result.data == NULL)
 				break;
+			if (is_tuple_temporary(result.data,
+					       space_rv->id,
+					       temp_space_ids))
+				continue;
 			rc = memtx_join_send_tuple(ctx->stream, space_rv->id,
 						   result.data, result.size);
 			if (rc != 0)
@@ -1383,6 +1435,7 @@ memtx_join_f(va_list ap)
 		if (rc != 0)
 			break;
 	}
+	mh_i32_delete(temp_space_ids);
 	return rc;
 }
 
diff --git a/src/box/schema_def.h b/src/box/schema_def.h
index 34529790d275db9490c7231ed988d49da036f45d..7d2bf8602b344d36ddd3337867e1fac9440d0782 100644
--- a/src/box/schema_def.h
+++ b/src/box/schema_def.h
@@ -57,7 +57,14 @@ enum {
 	/** Yet another arbitrary limit which simply needs to
 	 * exist.
 	 */
-	BOX_INDEX_PART_MAX = UINT8_MAX
+	BOX_INDEX_PART_MAX = UINT8_MAX,
+	/**
+	 * Start of the range of default temporary space ids.
+	 * By default they get ids from a special range to avoid conflicts with
+	 * spaces which could arrive via replication. But the user is free to
+	 * choose an id from outside this range.
+	 */
+	BOX_SPACE_ID_TEMPORARY_MIN = (1 << 30),
 };
 static_assert(BOX_INVALID_NAME_MAX <= BOX_NAME_MAX,
 	      "invalid name max is less than name max");
diff --git a/src/box/space.h b/src/box/space.h
index 36cbe1d8481384257629ecf9b704ad34e736b3cc..84d2f72c06a93d7df36811b467a09bdf7acfdf2a 100644
--- a/src/box/space.h
+++ b/src/box/space.h
@@ -347,6 +347,13 @@ space_is_data_temporary(const struct space *space)
 	return space_opts_is_data_temporary(&space->def->opts);
 }
 
+/** Return true if space is temporary. */
+static inline bool
+space_is_temporary(const struct space *space)
+{
+	return space_opts_is_temporary(&space->def->opts);
+}
+
 /** Return true if space is synchronous. */
 static inline bool
 space_is_sync(const struct space *space)
diff --git a/src/box/space_def.c b/src/box/space_def.c
index b698dd27e9f079cdec6e40c333c29da7db8e0c21..e148536db755c1f4e7e48dbcc99734724fbe7171 100644
--- a/src/box/space_def.c
+++ b/src/box/space_def.c
@@ -284,6 +284,7 @@ space_opts_parse_temporary(const char **data, void *vopts,
 const char *space_type_strs[] = {
 	/* [SPACE_TYPE_NORMAL]         = */ "normal",
 	/* [SPACE_TYPE_DATA_TEMPORARY] = */ "data-temporary",
+	/* [SPACE_TYPE_TEMPORARY]      = */ "temporary",
 };
 
 static int
@@ -311,3 +312,42 @@ space_opts_parse_type(const char **data, void *vopts, struct region *region)
 	opts->type = space_type;
 	return 0;
 }
+
+bool
+space_def_tuple_is_temporary(const char *data, uint32_t *space_id)
+{
+	uint32_t field_count = mp_decode_array(&data);
+	if (field_count < BOX_SPACE_FIELD_OPTS + 1)
+		return false;
+
+	static_assert(BOX_SPACE_FIELD_ID == 0,
+		      "the following code relies on this assumption");
+	if (mp_typeof(*data) != MP_UINT)
+		return false;
+	assert(space_id != NULL);
+	*space_id = mp_decode_uint(&data);
+
+	for (uint32_t i = 1; i < BOX_SPACE_FIELD_OPTS; i++)
+		mp_next(&data);
+	if (mp_typeof(*data) != MP_MAP)
+		return false;
+	uint32_t map_size = mp_decode_map(&data);
+
+	for (uint32_t i = 0; i < map_size; i++) {
+		if (mp_typeof(*data) != MP_STR)
+			return false;
+		uint32_t len;
+		const char *str = mp_decode_str(&data, &len);
+		if (len != strlen("type") || memcmp("type", str, len) != 0) {
+			mp_next(&data);
+			continue;
+		}
+		if (mp_typeof(*data) != MP_STR)
+			return false;
+		str = mp_decode_str(&data, &len);
+		enum space_type space_type = strnindex(space_type_strs, str,
+						       len, SPACE_TYPE_DEFAULT);
+		return space_type == SPACE_TYPE_TEMPORARY;
+	}
+	return false;
+}
diff --git a/src/box/space_def.h b/src/box/space_def.h
index e63b3cdf4474c2cf7989211d71dc9d1ec9ebc482..4e21f748a82c52e06fffc555502efd0f68ff09a5 100644
--- a/src/box/space_def.h
+++ b/src/box/space_def.h
@@ -56,6 +56,7 @@ enum space_type {
 	SPACE_TYPE_DEFAULT = -1,
 	SPACE_TYPE_NORMAL = 0,
 	SPACE_TYPE_DATA_TEMPORARY = 1,
+	SPACE_TYPE_TEMPORARY = 2,
 	space_type_MAX,
 };
 
@@ -80,6 +81,12 @@ struct space_opts {
 	 * - changes are not part of a snapshot
 	 * - in SQL: space_def memory is allocated on region and
 	 *   does not require manual release.
+	 *
+	 * If set to SPACE_TYPE_TEMPORARY:
+	 * - all of the above, but
+	 * - metadata is not persisted (doesn't exist at server start)
+	 * - metadata is not replicated (doesn't exist on replicas)
+	 * - this value cannot be changed from or to even for an empty space
 	 */
 	enum space_type type;
 	/**
@@ -138,6 +145,16 @@ space_opts_is_data_temporary(const struct space_opts *opts)
 	return opts->type != SPACE_TYPE_NORMAL;
 }
 
+/**
+ * Check if the space is temporary.
+ */
+static inline bool
+space_opts_is_temporary(const struct space_opts *opts)
+{
+	assert(opts->type != SPACE_TYPE_DEFAULT);
+	return opts->type == SPACE_TYPE_TEMPORARY;
+}
+
 /** Space metadata. */
 struct space_def {
 	/** Space id. */
@@ -233,6 +250,15 @@ space_tuple_format_new(struct tuple_format_vtab *vtab, void *engine,
 		       struct key_def *const *keys, uint16_t key_count,
 		       const struct space_def *def);
 
+/**
+ * Check if msgpack array pointed to by @a data represents a space definition
+ * tuple which corresponds to a temporary space.
+ * If @a space_id is not NULL the id of the space will be written into it in
+ * case of success.
+ */
+bool
+space_def_tuple_is_temporary(const char *data, uint32_t *space_id);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index bd452b43986b02f06742264ecffd8960206d0d1c..fecf59728cfd8cfe47cb4a3c680eb6562ef78550 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -4866,7 +4866,7 @@ selectExpander(Walker * pWalker, Select * p)
 	ExprList *pEList;
 	struct SrcList_item *pFrom;
 	Expr *pE, *pRight, *pExpr;
-	u16 selFlags = p->selFlags;
+	u32 selFlags = p->selFlags;
 
 	p->selFlags |= SF_Expanded;
 	if (NEVER(p->pSrc == 0) || (selFlags & SF_Expanded) != 0) {
diff --git a/src/box/sql/vdbe.c b/src/box/sql/vdbe.c
index 927affbcf9b5ef290839f4b190467047f72011eb..a7e57068b724e4d241f104a7e9c1fa9da75ac47a 100644
--- a/src/box/sql/vdbe.c
+++ b/src/box/sql/vdbe.c
@@ -4336,7 +4336,7 @@ case OP_GenSpaceid: {
 	assert(pOp->p1 > 0);
 	pOut = vdbe_prepare_null_out(p, pOp->p1);
 	uint32_t u;
-	if (box_generate_space_id(&u) != 0)
+	if (box_generate_space_id(&u, false) != 0)
 		goto abort_due_to_error;
 	mem_set_uint(pOut, u);
 	break;
diff --git a/src/box/tuple_constraint_fkey.c b/src/box/tuple_constraint_fkey.c
index a1a1209de36301ad02712263391011d1fa2d30c5..9c7c343839ca1ef6e528dd54dc2017dcf8c6329a 100644
--- a/src/box/tuple_constraint_fkey.c
+++ b/src/box/tuple_constraint_fkey.c
@@ -629,6 +629,12 @@ tuple_constraint_fkey_check_spaces(struct tuple_constraint *constr,
 				   struct space *space,
 				   struct space *foreign_space)
 {
+	if (space_is_temporary(foreign_space)) {
+		diag_set(ClientError, ER_CREATE_FOREIGN_KEY,
+			 constr->def.name, constr->space->def->name,
+			 "foreign space can't be temporary");
+		return -1;
+	}
 	if (space_is_data_temporary(foreign_space) &&
 	    !space_is_data_temporary(space)) {
 		diag_set(ClientError, ER_CREATE_FOREIGN_KEY,
diff --git a/src/box/txn.c b/src/box/txn.c
index b824ceff4f4b2e7fc0f91557574c7969ea922204..11837c0327b0850c9a4abd5c5bb24639e84639fb 100644
--- a/src/box/txn.c
+++ b/src/box/txn.c
@@ -877,7 +877,9 @@ txn_journal_entry_new(struct txn *txn)
 			rlist_splice(&txn->on_commit, &stmt->on_commit);
 		}
 
-		/* A read (e.g. select) request */
+		/* A read (e.g. select) request or
+		 * a temporary's space metadata update.
+		 */
 		if (stmt->row == NULL)
 			continue;
 
@@ -1554,3 +1556,12 @@ txn_attach(struct txn *txn)
 	trigger_add(&fiber()->on_yield, &txn->fiber_on_yield);
 	trigger_add(&fiber()->on_stop, &txn->fiber_on_stop);
 }
+
+void
+txn_stmt_mark_as_temporary(struct txn *txn, struct txn_stmt *stmt)
+{
+	assert(stmt->row != NULL);
+	/* Revert row counter increases. */
+	txn_update_row_counts(txn, stmt, -1);
+	stmt->row = NULL;
+}
diff --git a/src/box/txn.h b/src/box/txn.h
index e946f66cf5575b2494e0c18c8ff52dc2a0f4de16..f0fd45d95b237dc4d77202aecea0b404ff876c26 100644
--- a/src/box/txn.h
+++ b/src/box/txn.h
@@ -841,6 +841,24 @@ txn_is_fully_local(const struct txn *txn)
 	       txn->n_applier_rows == 0;
 }
 
+/**
+ * Mark @a stmt as temporary by removing the associated stmt->row
+ * and update @a txn accordingly.
+ *
+ * This function is called from on_replace_dd_* triggers to filter
+ * temporary space's metadata updates from WAL.
+ *
+ * NOTE: This could also be implemented by just not creating the stmt->row
+ * when txn_commit_stmt is called, but it would be less efficient that way
+ * as it would require putting an expensive check on a hot path.
+ * Doing the check inside the on_replace trigger is much cheaper
+ * as the data we're interested in (result of space_is_temporary(space))
+ * is readily available at that point.
+ * The downside of confusing control flow is outweighed by the efficiency.
+ */
+void
+txn_stmt_mark_as_temporary(struct txn *txn, struct txn_stmt *stmt);
+
 /**
  * End a statement. In autocommit mode, end
  * the current transaction as well.
diff --git a/test/box-luatest/fully-temporary_spaces_test.lua b/test/box-luatest/fully-temporary_spaces_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..9e6e108b482dab80cd3c8eecbec0cacac7f22a5e
--- /dev/null
+++ b/test/box-luatest/fully-temporary_spaces_test.lua
@@ -0,0 +1,523 @@
+local fio = require('fio')
+local xlog = require('xlog')
+local server = require('luatest.server')
+local replica_set = require('luatest.replica_set')
+local t = require('luatest')
+local g = t.group()
+local _ = nil
+
+g.before_all(function()
+    g.server = server:new({alias = 'master'})
+end
+)
+g.after_all(function()
+    _ = g.server.process and g.server:stop()
+    _ = g.replica_set and g.replica_set:drop()
+end)
+
+g.before_each(function()
+    if not g.server.process then
+        g.server:start()
+    end
+end)
+
+g.after_each(function()
+    _ = g.server.process and g.server:exec(function()
+        box.cfg { read_only = false }
+        for _, s in pairs({'test', 'datatemp', 'temp'}) do
+            local _ = box.space[s] and box.space[s]:drop()
+        end
+    end)
+end)
+
+-- check basic invariants of temporary spaces
+g.test_temporary_create = function()
+    g.server:exec(function()
+        local _space = box.space._space
+
+        local s = box.schema.space.create('temp', { type = 'temporary' })
+        t.assert_equals(s.temporary, true)
+        t.assert_equals(s.type, 'temporary')
+
+        --
+        -- Check automatic/manual space id.
+        --
+        s = box.schema.space.create('temp2', { type = 'temporary', id = 9999 })
+        t.assert_equals(s.type, 'temporary')
+        s:drop()
+
+        -- If id is not specified, it will be chosen in the special range.
+        s = box.schema.space.create('temp2', { type = 'temporary' })
+        t.assert_equals(s.id, box.space.temp.id + 1)
+
+        s = box.schema.space.create('temp3', { type = 'temporary' })
+        t.assert_equals(s.id, box.space.temp.id + 2)
+
+        -- Now we have an unoccupied space id box.space.temp.id + 1
+        box.space.temp2:drop()
+
+        -- But it's not chosen, because space ids grow, until they can't
+        s = box.schema.space.create('temp4', { type = 'temporary' })
+        t.assert_equals(s.id, box.space.temp.id + 3)
+        s:drop()
+
+        -- Now there's no more room to grow...
+        local BOX_SPACE_MAX = 0x7fffffff
+        box.schema.space.create('temp2', { type = 'temporary',
+                                           id = BOX_SPACE_MAX })
+
+        -- ... therefore we start filling in the gaps
+        s = box.schema.space.create('temp4', { type = 'temporary' })
+        t.assert_equals(s.id, box.space.temp.id + 1)
+        s:drop()
+
+        box.space.temp2:drop()
+        box.space.temp3:drop()
+
+        --
+        -- Here's all the ways you can create a data-temporary space now
+        --
+        s = box.schema.space.create('datatemp', { temporary = true })
+        t.assert_equals(s.temporary, true)
+        t.assert_equals(s.type, 'data-temporary')
+        s:drop()
+
+        s = box.schema.space.create('datatemp', { type = 'data-temporary' })
+        t.assert_equals(s.temporary, true)
+        t.assert_equals(s.type, 'data-temporary')
+        s:drop()
+
+        t.assert_error_msg_contains(
+            "only one of 'type' or 'temporary' may be specified",
+            box.schema.space.create, 'temp',
+            { type = 'data-temporary', temporary = true }
+        )
+
+        --
+        -- Here's all the ways you can create a normal space now
+        --
+        s = box.schema.space.create('normal', { temporary = false })
+        t.assert_equals(s.temporary, false)
+        t.assert_equals(s.type, 'normal')
+        s:drop()
+
+        s = box.schema.space.create('normal', { type = 'normal' })
+        t.assert_equals(s.temporary, false)
+        t.assert_equals(s.type, 'normal')
+        s:drop()
+
+        t.assert_error_msg_contains(
+            "only one of 'type' or 'temporary' may be specified",
+            box.schema.space.create, 'temp',
+            { type = 'normal', temporary = false }
+        )
+
+
+        t.assert_error_msg_contains(
+            "only one of 'type' or 'temporary' may be specified",
+            box.schema.space.create, 'temp',
+            { type = 'temporary', temporary = true }
+        )
+
+        t.assert_error_msg_contains(
+            "unknown space type, must be one of: 'normal', 'data-temporary'," ..
+            " 'temporary'",
+            box.schema.space.create, 'super', { type = 'super-temporary' }
+        )
+
+        t.assert_error_msg_contains(
+            "engine does not support data-temporary spaces",
+            box.schema.space.create,
+                'no-vinyl', { engine = 'vinyl', type = 'temporary' }
+        )
+
+        s = box.schema.space.create('datatemp', { type = 'data-temporary' })
+        t.assert_error_msg_contains(
+            "temporariness cannot change",
+            s.alter, s, { type = 'temporary' }
+        )
+        t.assert_error_msg_contains(
+            "temporariness cannot change",
+            _space.update,
+                _space, s.id, {{'=', 6, { type = 'temporary' }}}
+        )
+
+        s = box.space.temp
+        t.assert_error_msg_contains(
+            "temporariness cannot change",
+            s.alter, s, { type = 'data-temporary' }
+        )
+        t.assert_error_msg_contains(
+            "temporariness cannot change",
+            _space.update,
+                _space, s.id, {{'=', 6, { type = 'data-temporary' }}}
+        )
+    end)
+end
+
+-- check which features aren't supported for temporary spaces
+g.test_temporary_dont_support = function()
+    g.server:exec(function()
+        local s = box.schema.space.create('temp', { type = 'temporary',
+                                                        id = 1111111111 })
+        s:format {{'k', 'unsigned'}}
+        s:create_index('pk')
+        box.schema.func.create('foo', {
+            is_sandboxed = true,
+            is_deterministic = true,
+            body = "function() end"
+        })
+        t.assert_error_msg_equals(
+            "temporary space does not support functional indexes",
+            s.create_index, s, 'func', { func = 'foo' }
+        )
+
+        t.assert_error_msg_equals(
+            "temporary space does not support privileges",
+            box.schema.user.grant, 'guest', 'read', 'space', s.name
+        )
+    end)
+end
+
+-- check which sql features aren't supported for temporary spaces
+g.test_temporary_sql_dont_support = function()
+    g.server:exec(function()
+        local _space = box.space._space
+
+        box.schema.space.create('temp', { type = 'temporary',
+                                              id = 1111111112 })
+        box.schema.space.create('test', { format = { 'k', 'unsigned' } })
+
+        local function sql(stmt)
+            local ok, err = box.execute(stmt)
+            local _ = ok == nil and error(err)
+        end
+
+        t.assert_error_msg_contains(
+            "triggers are not supported for temporary spaces",
+            sql, [[
+                create trigger "my_trigger" before insert on "temp"
+                for each row begin
+                    insert into "temp" values (1);
+                end;
+            ]]
+        )
+
+        t.assert_error_msg_contains(
+            "sequences are not supported for temporary spaces",
+            sql, [[
+                alter table "temp"
+                    add column "k" integer primary key autoincrement
+            ]]
+        )
+
+        -- alter table works though
+        sql([[ alter table "temp" add column "k" int primary key ]])
+
+        t.assert_error_msg_contains(
+            "foreign space can't be temporary",
+            sql, [[
+                create table "my_table" (
+                    "id" integer primary key,
+                    "k" integer,
+                    foreign key ("k") references "temp" ("k")
+                )
+            ]]
+        )
+
+        t.assert_error_msg_contains(
+            "temporary space does not support constraints",
+            sql, [[
+                alter table "temp"
+                add constraint "fk" foreign key ("k") references "test" ("k")
+            ]]
+        )
+
+        -- CREATE VIEW
+
+        local function assert_create_view_not_supported(create_view_sql)
+            t.assert_error_msg_contains(
+                "CREATE VIEW does not support temporary spaces",
+                sql, create_view_sql
+            )
+            t.assert_error_msg_contains(
+                "CREATE VIEW does not support temporary spaces",
+                _space.insert,
+                    _space, {
+                        9999, 1, 'my_view', 'memtx', 1,
+                        { sql = create_view_sql, view = true },
+                        {{ type = 'unsigned', name = 'ID',
+                           nullable_action = 'none', is_nullable = true }},
+                    }
+            )
+        end
+
+        box.space._session_settings:update({'sql_seq_scan'}, {{'=', 2, true}})
+        assert_create_view_not_supported([[
+            create view "my_view" as select * from "temp"
+        ]])
+
+        assert_create_view_not_supported([[
+            create view "my_view" as
+            select * from (select * from (select * from "temp"))
+        ]])
+
+        assert_create_view_not_supported([[
+            create view "my_view" as
+            select * from (select 1 x) where x in (select * from "temp")
+        ]])
+
+        assert_create_view_not_supported([[
+            create view "my_view" as values ((select * from "temp"))
+        ]])
+
+        assert_create_view_not_supported([[
+            create view "my_view" as
+            values (1) union select * from "temp"
+        ]])
+
+        -- CHECK constraints
+
+        t.assert_error_msg_contains(
+            "temporary space does not support constraints",
+            sql, [[
+                alter table "temp" add constraint "c2" check ( "k" > 0 )
+            ]]
+        )
+    end)
+end
+
+-- check that CRUD operations on space meta-data works in read-only mode
+g.test_temporary_read_only = function()
+    g.server:exec(function()
+        local _space, _index = box.space._space, box.space._index
+        box.cfg { read_only = true }
+        t.assert(box.info.ro)
+
+        t.assert_error_msg_contains(
+            "Can't modify data on a read-only instance",
+            box.schema.space.create, 'datatemp', { temporary = true }
+        )
+
+        -- space create works
+        local s = box.schema.space.create('temp', { type = 'temporary',
+                                                        id = 1111111113 })
+
+        -- space rename works
+        s:rename('newname')
+        t.assert_equals(s.name, box.space.newname.name)
+        _space:update(s.id, {{'=', 3, 'temp'}})
+        t.assert_equals(s.name, box.space.temp.name)
+
+        -- format change works
+        t.assert_equals(s:format(), {})
+        s:format {{'k', 'number'}}
+        t.assert_equals(s:format(), {{name = 'k', type = 'number'}})
+        s:alter { format = {{'k', 'number'}, {'v', 'any'}} }
+        t.assert_equals(
+            s:format(),
+            {{name = 'k', type = 'number'}, {name = 'v', type = 'any'}}
+        )
+
+        -- index create works
+        local i = s:create_index('first', { type = 'HASH' })
+        t.assert(s.index.first)
+
+        -- index rename works
+        i:rename('pk')
+        t.assert_equals(i.name, s.index.pk.name)
+        _index:update({s.id, i.id}, {{'=', 3, 'first'}})
+        t.assert_equals(i.name, s.index.first.name)
+
+        -- index alter works
+        t.assert_equals(i.type, 'HASH')
+        i:alter { type = 'TREE' }
+        t.assert_equals(i.type, 'TREE')
+
+        -- on_replace triggers even work
+        local tbl = {}
+        s:on_replace(function(_, new_tuple) table.insert(tbl, new_tuple) end)
+        t.assert_equals(#s:on_replace(), 1)
+        s:auto_increment{'foo'}
+        t.assert_equals(tbl, {{1, 'foo'}})
+        s:on_replace(nil, s:on_replace()[1])
+        t.assert_equals(#s:on_replace(), 0)
+
+        -- truncate works
+        t.assert_equals(s:len(), 1)
+        s:truncate()
+        t.assert_equals(s:len(), 0)
+
+        box.space._session_settings:update({'sql_seq_scan'}, {{'=', 2, true}})
+        -- basic sql works
+        box.execute [[ insert into "temp" values (420, 69), (13, 37) ]]
+        t.assert_equals(
+            box.execute [[ select * from "temp" ]].rows,
+            {{13, 37}, {420, 69}}
+        )
+        box.execute [[ truncate table "temp" ]]
+        local count = box.execute [[ select count(*) from "temp" ]].rows[1]
+        t.assert_equals(count, {0})
+        box.execute [[ drop table "temp" ]]
+        t.assert(not box.space.temp)
+
+        s = box.schema.space.create('temp', { type = 'temporary',
+                                                  id = 1111111114 })
+
+        -- all kinds of indexes work
+        s:create_index('tree', { type = 'TREE' })
+        s:create_index('hash', { type = 'HASH' })
+        s:create_index('rtree', {
+            type = 'RTREE', unique = false, parts = {2, 'array'}
+        })
+        s:create_index('bitset', {
+            type = 'BITSET', unique = false, parts = {3, 'unsigned'}
+        })
+
+        local row = box.tuple.new {1, {2, 3}, 4}
+        s:insert(row)
+
+        t.assert_equals(s.index.hash:get(1), row)
+        t.assert_equals(s.index.tree:get(1), row)
+        t.assert_equals(s.index.rtree:select({2, 3}), {row})
+        t.assert_equals(s.index.bitset:select(4), {row})
+
+        s:truncate()
+
+        -- index drop works
+        s.index.bitset:drop()
+        s.index.rtree:drop()
+        s.index.hash:drop()
+        s.index.tree:drop()
+
+        -- space drop works
+        s:drop()
+    end)
+end
+
+-- check temporary space definitions aren't replicated
+g.test_meta_data_not_replicated = function()
+    g.server:stop()
+    g.replica_set = replica_set:new{}
+    local replication = {
+        server.build_listen_uri('master', g.replica_set.id),
+        server.build_listen_uri('replica_1', g.replica_set.id),
+    }
+    g.replica_set:build_and_add_server{
+        alias = 'master',
+        box_cfg = { read_only = false, replication = replication },
+    }
+    g.replica_set:build_and_add_server{
+        alias = 'replica_1',
+        box_cfg = { read_only = true, replication = replication },
+    }
+    g.replica_set:start()
+
+    g.master = g.replica_set:get_server('master')
+    g.master:exec(function()
+        box.schema.space.create('temp', { type = 'temporary',
+                                              id = 1111111115 })
+        box.schema.space.create('datatemp', { temporary = true })
+    end)
+
+    g.replica_1 = g.replica_set:get_server('replica_1')
+    g.replica_1:wait_for_vclock_of(g.master)
+    -- temporary is not replicated via relay
+    g.replica_1:exec(function()
+        t.assert(box.space.datatemp)
+        t.assert(not box.space.temp)
+    end)
+
+    g.replica_2 = g.replica_set:build_and_add_server{
+        alias = 'replica_2',
+        box_cfg = { replication = replication },
+    }
+    g.replica_2:start{wait_until_ready = true}
+    g.replica_2:wait_for_vclock_of(g.master)
+    -- temporary is not replicated via snapshot
+    g.replica_2:exec(function()
+        t.assert(box.space.datatemp)
+        t.assert(not box.space.temp)
+    end)
+end
+
+g.after_test('test_meta_data_not_replicated', function()
+    g.replica_set:drop()
+end)
+
+local function check_wal_and_snap(expected)
+    local id_min = g.server:eval 'return box.schema.SPACE_ID_TEMPORARY_MIN'
+
+    local function is_meta_local_space_id(id)
+        return type(id) == 'number' and (id >= id_min or id == 9999)
+    end
+
+    local function contains_meta_local_space_id(entry)
+        local key, tuple = entry.BODY.key, entry.BODY.tuple
+        return key and key:pairs():any(is_meta_local_space_id)
+            or tuple and tuple:pairs():any(is_meta_local_space_id)
+    end
+
+    for _, f in pairs(fio.listdir(g.server.workdir)) do
+        if not f:endswith('.xlog') and not f:endswith('.xlog') then
+            goto continue
+        end
+
+        local path = fio.pathjoin(g.server.workdir, f)
+        local x = xlog.pairs(path)
+            :filter(contains_meta_local_space_id)
+            :totable()
+        if #x > 0 then
+            x = x[1]
+            x = {
+                type = x.HEADER.type,
+                space_id = x.BODY.space_id,
+                tuple = x.BODY.tuple,
+                key = x.BODY.key,
+            }
+            t.assert_equals({f, x}, {f, expected})
+        end
+
+        ::continue::
+    end
+end
+
+-- check temporary space definitions aren't persisted
+g.test_meta_data_not_persisted = function()
+    g.server:exec(function()
+        -- temporary space's metadata will go into WAL and snapshot
+        local s = box.schema.space.create('datatemp', { temporary = true })
+        s:create_index('pk')
+        s:put{1,2,3}
+        s:truncate()
+
+        -- temporary space's metadata will not go into WAL or snapshot
+        s = box.schema.space.create('temp', { type = 'temporary',
+                                                  id = 1111111116 })
+        s:create_index('pk')
+        s:put{1,2,3}
+        s:truncate()
+
+        -- there's nothing special about space ids
+        s = box.schema.space.create('test', { type = 'temporary',
+                                              id = 9999 })
+        s:create_index('pk')
+        s:put{1,2,3}
+        s:truncate()
+    end)
+
+    check_wal_and_snap({})
+
+    g.server:restart()
+
+    g.server:exec(function()
+        -- temporary space exists after restart
+        t.assert(box.space.datatemp)
+        -- temporary space doesn't
+        t.assert(not box.space.temp)
+
+        box.snapshot()
+    end)
+
+    check_wal_and_snap({})
+end
diff --git a/test/box/access.result b/test/box/access.result
index 684b48282bd29e23da432e82541fad631e37995c..fc4f7478bd0685a4edb46c9380948ca20185d3d3 100644
--- a/test/box/access.result
+++ b/test/box/access.result
@@ -1519,6 +1519,9 @@ box.schema.user.grant("tester", "read", "space", "_user")
 box.schema.user.grant("tester", "read", "space", "_func")
 ---
 ...
+box.schema.user.grant("tester", "read", "space", "_truncate")
+---
+...
 -- failed create
 box.session.su("tester")
 ---
diff --git a/test/box/access.test.lua b/test/box/access.test.lua
index 0c0a885865f07997c9db887847cb25a465a181c7..29e092ac39377a551d0ba8bed220c1682e0635c6 100644
--- a/test/box/access.test.lua
+++ b/test/box/access.test.lua
@@ -588,6 +588,7 @@ box.schema.func.create('test_func')
 box.session.su("admin")
 box.schema.user.grant("tester", "read", "space", "_user")
 box.schema.user.grant("tester", "read", "space", "_func")
+box.schema.user.grant("tester", "read", "space", "_truncate")
 -- failed create
 box.session.su("tester")
 box.schema.space.create("test_space")
diff --git a/test/box/access_misc.result b/test/box/access_misc.result
index c4b64e2bb7f73ee00ea8d05a56f239c52afa4146..be2464104f95db6613121d802b2d4f1972198f1f 100644
--- a/test/box/access_misc.result
+++ b/test/box/access_misc.result
@@ -78,7 +78,7 @@ s:delete(1)
 ...
 s:drop()
 ---
-- error: Write access to space '_space_sequence' is denied for user 'testus'
+- error: Read access to space '_space_sequence' is denied for user 'testus'
 ...
 --
 -- Check double revoke
@@ -126,7 +126,7 @@ s:insert({3})
 ...
 s:drop()
 ---
-- error: Write access to space '_space_sequence' is denied for user 'testus'
+- error: Read access to space '_space_sequence' is denied for user 'testus'
 ...
 session.su('admin')
 ---
@@ -169,7 +169,7 @@ s:delete({3})
 ...
 s:drop()
 ---
-- error: Write access to space '_space_sequence' is denied for user 'guest'
+- error: Read access to space '_space_sequence' is denied for user 'guest'
 ...
 gs = box.schema.space.create('guest_space')
 ---
diff --git a/test/box/alter.test.lua b/test/box/alter.test.lua
index 9413bf0f6b904e107b0f9277dedc680773c9568b..8ebf3d9daf6d3c96a0f60616f4ae9403748dce38 100644
--- a/test/box/alter.test.lua
+++ b/test/box/alter.test.lua
@@ -660,4 +660,4 @@ s.index.test1:bsize() < s.index.test2:bsize()
 s.index.test1:bsize() == s.index.test33:bsize()
 s.index.test1:bsize() < s.index.test4:bsize()
 
-s:drop()
\ No newline at end of file
+s:drop()
diff --git a/test/box/stat.result b/test/box/stat.result
index 9bd9bb22b5dd9e4932358a7b60292f8db0b8a7d9..45b51e952910b67661d165c6ed5ff2ec55dba61a 100644
--- a/test/box/stat.result
+++ b/test/box/stat.result
@@ -33,6 +33,11 @@ box.stat.ERROR.total
 space = box.schema.space.create('tweedledum')
 ---
 ...
+-- create_space performs a select when choosing a new space id
+box.stat.SELECT.total
+---
+- 1
+...
 index = space:create_index('primary', { type = 'hash' })
 ---
 ...
@@ -59,7 +64,7 @@ box.stat.REPLACE.total
 ...
 box.stat.SELECT.total
 ---
-- 4
+- 5
 ...
 -- check exceptions
 space:get('Impossible value')
@@ -77,14 +82,14 @@ space:get(1)
 ...
 box.stat.SELECT.total
 ---
-- 5
+- 6
 ...
 space:get(11)
 ---
 ...
 box.stat.SELECT.total
 ---
-- 6
+- 7
 ...
 space:select(5)
 ---
@@ -92,7 +97,7 @@ space:select(5)
 ...
 box.stat.SELECT.total
 ---
-- 7
+- 8
 ...
 space:select(15)
 ---
@@ -100,14 +105,14 @@ space:select(15)
 ...
 box.stat.SELECT.total
 ---
-- 8
+- 9
 ...
 for _ in space:pairs() do end
 ---
 ...
 box.stat.SELECT.total
 ---
-- 9
+- 10
 ...
 -- reset
 box.stat.reset()
diff --git a/test/box/stat.test.lua b/test/box/stat.test.lua
index 6ee1ec440bbaa4220b0133bf9e20552a5d6ba2fa..ab4b3d134f71b4f89e59b5b323bb404dac63b0bd 100644
--- a/test/box/stat.test.lua
+++ b/test/box/stat.test.lua
@@ -11,6 +11,9 @@ box.stat.SELECT.total
 box.stat.ERROR.total
 
 space = box.schema.space.create('tweedledum')
+-- create_space performs a select when choosing a new space id
+box.stat.SELECT.total
+
 index = space:create_index('primary', { type = 'hash' })
 
 -- check stat_cleanup