From c883a49a4096b171f324d44c9fcacda55ed361ee Mon Sep 17 00:00:00 2001
From: Dessalines <dessalines@users.noreply.github.com>
Date: Wed, 15 Dec 2021 14:49:59 -0500
Subject: [PATCH] First pass at invite-only migration. (#1949)

* First pass at invite-only migration.

* Implement email verification (fixes #219)

* remove unwrap

* Adding views and functionality to registration application. #209

* Add private instance site column, and back end checks.

* Adding some message fields to LoginResponse

* Adding private instance to site setup.

* A few additions:

- Add a DeleteAccount response.
- RegistrationApplicationView now has the safe LocalUserSettings.
- Adding VerifyEmail to websocket API, added a proper response type.

* Adding and reorganizing some email helpers.

* A few fixes for private sites:

- Added a check_registration_application function.
- Only send a verification email if its been changed.
- VerifyEmail now returns LoginResponse.
- Deleting the old tokens after a successful email verify.
- If port is missing on email config, display a better error message.

* Version 0.15.0-rc.3

* Adding published to email_verification table.

* Adding fixes from comments.

* Version 0.15.0-rc.4

* Adding modlog private site check.

* Version 0.15.0-rc.6

Co-authored-by: Felix Ableitner <me@nutomic.com>
---
 Cargo.lock                                    |  28 +-
 Cargo.toml                                    |  26 +-
 config/defaults.hjson                         |   4 +
 crates/api/Cargo.toml                         |  20 +-
 crates/api/src/lib.rs                         |  16 +-
 crates/api/src/local_user.rs                  | 186 +++++---
 crates/api/src/site.rs                        | 164 +++++++-
 crates/api_common/Cargo.toml                  |  12 +-
 crates/api_common/src/lib.rs                  | 185 +++++++-
 crates/api_common/src/person.rs               |  19 +-
 crates/api_common/src/site.rs                 |  47 +++
 crates/api_crud/Cargo.toml                    |  20 +-
 crates/api_crud/src/comment/read.rs           |  11 +-
 crates/api_crud/src/community/read.rs         |  12 +-
 crates/api_crud/src/post/read.rs              |  12 +-
 crates/api_crud/src/private_message/create.rs |   7 +-
 crates/api_crud/src/site/create.rs            |   2 +-
 crates/api_crud/src/site/read.rs              |  13 +-
 crates/api_crud/src/site/update.rs            |  40 +-
 crates/api_crud/src/user/create.rs            | 103 +++--
 crates/api_crud/src/user/delete.rs            |  10 +-
 crates/api_crud/src/user/read.rs              |   9 +-
 crates/apub/Cargo.toml                        |  16 +-
 crates/apub_lib/Cargo.toml                    |   6 +-
 crates/apub_lib_derive/Cargo.toml             |   2 +-
 crates/db_schema/Cargo.toml                   |   6 +-
 .../src/aggregates/site_aggregates.rs         |   4 +
 .../db_schema/src/impls/email_verification.rs |  55 +++
 crates/db_schema/src/impls/local_user.rs      |  24 +-
 crates/db_schema/src/impls/mod.rs             |   2 +
 .../src/impls/password_reset_request.rs       |   4 +-
 .../src/impls/registration_application.rs     |  42 ++
 crates/db_schema/src/newtypes.rs              |   4 +-
 crates/db_schema/src/schema.rs                |  32 ++
 .../src/source/email_verification.rs          |  19 +
 crates/db_schema/src/source/local_user.rs     |  10 +-
 crates/db_schema/src/source/mod.rs            |   2 +
 .../src/source/registration_application.rs    |  25 ++
 crates/db_schema/src/source/site.rs           |  10 +-
 crates/db_views/Cargo.toml                    |   4 +-
 crates/db_views/src/lib.rs                    |   1 +
 .../src/registration_application_view.rs      | 396 ++++++++++++++++++
 crates/db_views_actor/Cargo.toml              |   4 +-
 crates/db_views_moderator/Cargo.toml          |   4 +-
 crates/routes/Cargo.toml                      |  16 +-
 crates/utils/Cargo.toml                       |   2 +-
 crates/utils/src/email.rs                     |  17 +-
 crates/utils/src/settings/structs.rs          |   8 +
 crates/websocket/Cargo.toml                   |  12 +-
 crates/websocket/src/lib.rs                   |   4 +
 crates/websocket/src/send.rs                  |  46 +-
 .../down.sql                                  |   8 +
 .../up.sql                                    |  14 +
 .../down.sql                                  |   9 +
 .../up.sql                                    |  19 +
 .../down.sql                                  |   1 +
 .../up.sql                                    |   1 +
 src/api_routes.rs                             |  19 +-
 src/main.rs                                   |   4 +-
 59 files changed, 1540 insertions(+), 258 deletions(-)
 create mode 100644 crates/db_schema/src/impls/email_verification.rs
 create mode 100644 crates/db_schema/src/impls/registration_application.rs
 create mode 100644 crates/db_schema/src/source/email_verification.rs
 create mode 100644 crates/db_schema/src/source/registration_application.rs
 create mode 100644 crates/db_views/src/registration_application_view.rs
 create mode 100644 migrations/2021-11-23-132840_email_verification/down.sql
 create mode 100644 migrations/2021-11-23-132840_email_verification/up.sql
 create mode 100644 migrations/2021-11-23-153753_add_invite_only_columns/down.sql
 create mode 100644 migrations/2021-11-23-153753_add_invite_only_columns/up.sql
 create mode 100644 migrations/2021-12-09-225529_add_published_to_email_verification/down.sql
 create mode 100644 migrations/2021-12-09-225529_add_published_to_email_verification/up.sql

diff --git a/Cargo.lock b/Cargo.lock
index 9e1bab42..07ad6d2e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1686,7 +1686,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "lemmy_api"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "actix",
  "actix-rt",
@@ -1729,7 +1729,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_api_common"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "actix-web",
  "chrono",
@@ -1747,7 +1747,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_api_crud"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "actix",
  "actix-rt",
@@ -1790,7 +1790,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_apub"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "activitystreams-kinds",
  "actix",
@@ -1836,7 +1836,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_apub_lib"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "activitystreams",
  "actix-web",
@@ -1863,7 +1863,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_apub_lib_derive"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "proc-macro2 1.0.33",
  "quote 1.0.10",
@@ -1873,7 +1873,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_db_schema"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "bcrypt",
  "chrono",
@@ -1895,7 +1895,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_db_views"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "diesel",
  "lemmy_db_schema",
@@ -1907,7 +1907,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_db_views_actor"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "diesel",
  "lemmy_db_schema",
@@ -1916,7 +1916,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_db_views_moderator"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "diesel",
  "lemmy_db_schema",
@@ -1925,7 +1925,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_routes"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "actix",
  "actix-http",
@@ -1956,7 +1956,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_server"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "activitystreams",
  "actix",
@@ -2000,7 +2000,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_utils"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "actix-rt",
  "actix-web",
@@ -2038,7 +2038,7 @@ dependencies = [
 
 [[package]]
 name = "lemmy_websocket"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 dependencies = [
  "actix",
  "actix-web",
diff --git a/Cargo.toml b/Cargo.toml
index aa4fa775..817fb7f6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_server"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -31,18 +31,18 @@ members = [
 ]
 
 [dependencies]
-lemmy_api = { version = "=0.14.4-rc.4", path = "./crates/api" }
-lemmy_api_crud = { version = "=0.14.4-rc.4", path = "./crates/api_crud" }
-lemmy_apub = { version = "=0.14.4-rc.4", path = "./crates/apub" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "./crates/apub_lib" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "./crates/utils" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "./crates/db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "./crates/db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "./crates/db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "./crates/db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "crates/api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "./crates/websocket" }
-lemmy_routes = { version = "=0.14.4-rc.4", path = "./crates/routes" }
+lemmy_api = { version = "=0.15.0-rc.6", path = "./crates/api" }
+lemmy_api_crud = { version = "=0.15.0-rc.6", path = "./crates/api_crud" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "./crates/apub" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "./crates/apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "./crates/utils" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "./crates/db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "./crates/db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "./crates/db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "./crates/db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "crates/api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "./crates/websocket" }
+lemmy_routes = { version = "=0.15.0-rc.6", path = "./crates/routes" }
 diesel = "1.4.8"
 diesel_migrations = "1.4.0"
 chrono = { version = "0.4.19", features = ["serde"] }
diff --git a/config/defaults.hjson b/config/defaults.hjson
index 1126fef3..9edf9fc0 100644
--- a/config/defaults.hjson
+++ b/config/defaults.hjson
@@ -97,6 +97,10 @@
     open_registration: true
     enable_nsfw: true
     community_creation_admin_only: true
+    require_email_verification: true
+    require_application: true
+    application_question: "string"
+    private_instance: true
   }
   # the domain name of your instance (mandatory)
   hostname: "unset"
diff --git a/crates/api/Cargo.toml b/crates/api/Cargo.toml
index 907dec10..d7258e5c 100644
--- a/crates/api/Cargo.toml
+++ b/crates/api/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_api"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -13,15 +13,15 @@ path = "src/lib.rs"
 doctest = false
 
 [dependencies]
-lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
 diesel = "1.4.8"
 bcrypt = "0.10.1"
 chrono = { version = "0.4.19", features = ["serde"] }
diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs
index d535c467..26a41d3d 100644
--- a/crates/api/src/lib.rs
+++ b/crates/api/src/lib.rs
@@ -38,6 +38,15 @@ pub async fn match_websocket_operation(
     UserOperation::GetCaptcha => do_websocket_operation::<GetCaptcha>(context, id, op, data).await,
     UserOperation::GetReplies => do_websocket_operation::<GetReplies>(context, id, op, data).await,
     UserOperation::AddAdmin => do_websocket_operation::<AddAdmin>(context, id, op, data).await,
+    UserOperation::GetUnreadRegistrationApplicationCount => {
+      do_websocket_operation::<GetUnreadRegistrationApplicationCount>(context, id, op, data).await
+    }
+    UserOperation::ListRegistrationApplications => {
+      do_websocket_operation::<ListRegistrationApplications>(context, id, op, data).await
+    }
+    UserOperation::ApproveRegistrationApplication => {
+      do_websocket_operation::<ApproveRegistrationApplication>(context, id, op, data).await
+    }
     UserOperation::BanPerson => do_websocket_operation::<BanPerson>(context, id, op, data).await,
     UserOperation::BlockPerson => {
       do_websocket_operation::<BlockPerson>(context, id, op, data).await
@@ -75,6 +84,9 @@ pub async fn match_websocket_operation(
     UserOperation::GetUnreadCount => {
       do_websocket_operation::<GetUnreadCount>(context, id, op, data).await
     }
+    UserOperation::VerifyEmail => {
+      do_websocket_operation::<VerifyEmail>(context, id, op, data).await
+    }
 
     // Private Message ops
     UserOperation::MarkPrivateMessageAsRead => {
@@ -219,8 +231,8 @@ mod tests {
     let inserted_person = Person::create(&conn, &new_person).unwrap();
 
     let local_user_form = LocalUserForm {
-      person_id: inserted_person.id,
-      password_encrypted: "123456".to_string(),
+      person_id: Some(inserted_person.id),
+      password_encrypted: Some("123456".to_string()),
       ..LocalUserForm::default()
     };
 
diff --git a/crates/api/src/local_user.rs b/crates/api/src/local_user.rs
index a0711d69..781a581a 100644
--- a/crates/api/src/local_user.rs
+++ b/crates/api/src/local_user.rs
@@ -6,10 +6,14 @@ use captcha::{gen, Difficulty};
 use chrono::Duration;
 use lemmy_api_common::{
   blocking,
+  check_registration_application,
   get_local_user_view_from_jwt,
   is_admin,
   password_length_check,
   person::*,
+  send_email_verification_success,
+  send_password_reset_email,
+  send_verification_email,
 };
 use lemmy_db_schema::{
   diesel_option_overwrite,
@@ -19,6 +23,7 @@ use lemmy_db_schema::{
   source::{
     comment::Comment,
     community::Community,
+    email_verification::EmailVerification,
     local_user::{LocalUser, LocalUserForm},
     moderator::*,
     password_reset_request::*,
@@ -46,12 +51,10 @@ use lemmy_db_views_actor::{
 };
 use lemmy_utils::{
   claims::Claims,
-  email::send_email,
   location_info,
-  utils::{generate_random_string, is_valid_display_name, is_valid_matrix_id, naive_from_unix},
+  utils::{is_valid_display_name, is_valid_matrix_id, naive_from_unix},
   ConnectionId,
   LemmyError,
-  Sensitive,
 };
 use lemmy_websocket::{
   messages::{CaptchaItem, SendAllMessage},
@@ -90,14 +93,25 @@ impl Perform for Login {
       return Err(LemmyError::from_message("password_incorrect"));
     }
 
+    let site = blocking(context.pool(), Site::read_simple).await??;
+    if site.require_email_verification && !local_user_view.local_user.email_verified {
+      return Err(LemmyError::from_message("email_not_verified"));
+    }
+
+    check_registration_application(&site, &local_user_view, context.pool()).await?;
+
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        local_user_view.local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          local_user_view.local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -164,11 +178,35 @@ impl Perform for SaveUserSettings {
 
     let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
     let banner = diesel_option_overwrite_to_url(&data.banner)?;
-    let email = diesel_option_overwrite(&data.email.clone().map(Sensitive::into_inner));
     let bio = diesel_option_overwrite(&data.bio);
     let display_name = diesel_option_overwrite(&data.display_name);
     let matrix_user_id = diesel_option_overwrite(&data.matrix_user_id);
     let bot_account = data.bot_account;
+    let email_deref = data.email.as_deref().map(|e| e.to_owned());
+    let email = diesel_option_overwrite(&email_deref);
+
+    if let Some(Some(email)) = &email {
+      let previous_email = local_user_view.local_user.email.unwrap_or_default();
+      // Only send the verification email if there was an email change
+      if previous_email.ne(email) {
+        send_verification_email(
+          local_user_view.local_user.id,
+          email,
+          &local_user_view.person.name,
+          context.pool(),
+          &context.settings(),
+        )
+        .await?;
+      }
+    }
+
+    // When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
+    if let Some(email) = &email {
+      let site_fut = blocking(context.pool(), Site::read_simple);
+      if email.is_none() && site_fut.await??.require_email_verification {
+        return Err(LemmyError::from_message("email_required"));
+      }
+    }
 
     if let Some(Some(bio)) = &bio {
       if bio.chars().count() > 300 {
@@ -228,9 +266,9 @@ impl Perform for SaveUserSettings {
     .map_err(|e| e.with_message("user_already_exists"))?;
 
     let local_user_form = LocalUserForm {
-      person_id,
+      person_id: Some(person_id),
       email,
-      password_encrypted,
+      password_encrypted: Some(password_encrypted),
       show_nsfw: data.show_nsfw,
       show_bot_accounts: data.show_bot_accounts,
       show_scores: data.show_scores,
@@ -242,6 +280,8 @@ impl Perform for SaveUserSettings {
       show_read_posts: data.show_read_posts,
       show_new_post_notifs: data.show_new_post_notifs,
       send_notifications_to_email: data.send_notifications_to_email,
+      email_verified: None,
+      accepted_application: None,
     };
 
     let local_user_res = blocking(context.pool(), move |conn| {
@@ -265,12 +305,16 @@ impl Perform for SaveUserSettings {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        updated_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -315,12 +359,16 @@ impl Perform for ChangePassword {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        updated_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -736,34 +784,8 @@ impl Perform for PasswordReset {
     .map_err(LemmyError::from)
     .map_err(|e| e.with_message("couldnt_find_that_username_or_email"))?;
 
-    // Generate a random token
-    let token = generate_random_string();
-
-    // Insert the row
-    let token2 = token.clone();
-    let local_user_id = local_user_view.local_user.id;
-    blocking(context.pool(), move |conn| {
-      PasswordResetRequest::create_token(conn, local_user_id, &token2)
-    })
-    .await??;
-
     // Email the pure token to the user.
-    // TODO no i18n support here.
-    let email = &local_user_view.local_user.email.expect("email");
-    let subject = &format!("Password reset for {}", local_user_view.person.name);
-    let protocol_and_hostname = &context.settings().get_protocol_and_hostname();
-    let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", local_user_view.person.name, protocol_and_hostname, &token);
-    send_email(
-      subject,
-      email,
-      &local_user_view.person.name,
-      html,
-      &context.settings(),
-    )
-    .map_err(|e| anyhow::anyhow!("{}", e))
-    .map_err(LemmyError::from)
-    .map_err(|e| e.with_message("email_send_failed"))?;
-
+    send_password_reset_email(&local_user_view, context.pool(), &context.settings()).await?;
     Ok(PasswordResetResponse {})
   }
 }
@@ -805,12 +827,16 @@ impl Perform for PasswordChange {
 
     // Return the jwt
     Ok(LoginResponse {
-      jwt: Claims::jwt(
-        updated_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
+      jwt: Some(
+        Claims::jwt(
+          updated_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      ),
+      verify_email_sent: false,
+      registration_created: false,
     })
   }
 }
@@ -893,3 +919,49 @@ impl Perform for GetUnreadCount {
     Ok(res)
   }
 }
+
+#[async_trait::async_trait(?Send)]
+impl Perform for VerifyEmail {
+  type Response = VerifyEmailResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<usize>,
+  ) -> Result<Self::Response, LemmyError> {
+    let token = self.token.clone();
+    let verification = blocking(context.pool(), move |conn| {
+      EmailVerification::read_for_token(conn, &token)
+    })
+    .await?
+    .map_err(LemmyError::from)
+    .map_err(|e| e.with_message("token_not_found"))?;
+
+    let form = LocalUserForm {
+      // necessary in case this is a new signup
+      email_verified: Some(true),
+      // necessary in case email of an existing user was changed
+      email: Some(Some(verification.email)),
+      ..LocalUserForm::default()
+    };
+    let local_user_id = verification.local_user_id;
+    blocking(context.pool(), move |conn| {
+      LocalUser::update(conn, local_user_id, &form)
+    })
+    .await??;
+
+    let local_user_view = blocking(context.pool(), move |conn| {
+      LocalUserView::read(conn, local_user_id)
+    })
+    .await??;
+
+    send_email_verification_success(&local_user_view, &context.settings())?;
+
+    blocking(context.pool(), move |conn| {
+      EmailVerification::delete_old_tokens_for_local_user(conn, local_user_id)
+    })
+    .await??;
+
+    Ok(VerifyEmailResponse {})
+  }
+}
diff --git a/crates/api/src/site.rs b/crates/api/src/site.rs
index 99906bf6..fdcd91cf 100644
--- a/crates/api/src/site.rs
+++ b/crates/api/src/site.rs
@@ -5,9 +5,11 @@ use diesel::NotFound;
 use lemmy_api_common::{
   blocking,
   build_federated_instances,
+  check_private_instance,
   get_local_user_view_from_jwt,
   get_local_user_view_from_jwt_opt,
   is_admin,
+  send_application_approved_email,
   site::*,
 };
 use lemmy_apub::{
@@ -19,9 +21,15 @@ use lemmy_apub::{
   EndpointType,
 };
 use lemmy_db_schema::{
+  diesel_option_overwrite,
   from_opt_str_to_opt_enum,
   newtypes::PersonId,
-  source::{moderator::*, site::Site},
+  source::{
+    local_user::{LocalUser, LocalUserForm},
+    moderator::*,
+    registration_application::{RegistrationApplication, RegistrationApplicationForm},
+    site::Site,
+  },
   traits::{Crud, DeleteableOrRemoveable},
   DbPool,
   ListingType,
@@ -30,7 +38,12 @@ use lemmy_db_schema::{
 };
 use lemmy_db_views::{
   comment_view::{CommentQueryBuilder, CommentView},
+  local_user_view::LocalUserView,
   post_view::{PostQueryBuilder, PostView},
+  registration_application_view::{
+    RegistrationApplicationQueryBuilder,
+    RegistrationApplicationView,
+  },
   site_view::SiteView,
 };
 use lemmy_db_views_actor::{
@@ -64,6 +77,12 @@ impl Perform for GetModlog {
   ) -> Result<GetModlogResponse, LemmyError> {
     let data: &GetModlog = self;
 
+    let local_user_view =
+      get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
+        .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let community_id = data.community_id;
     let mod_person_id = data.mod_person_id;
     let page = data.page;
@@ -149,6 +168,8 @@ impl Perform for Search {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
     let show_bot_accounts = local_user_view
       .as_ref()
@@ -388,6 +409,8 @@ impl Perform for ResolveObject {
     let local_user_view =
       get_local_user_view_from_jwt_opt(self.auth.as_ref(), context.pool(), context.secret())
         .await?;
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let res = search_by_apub_id(&self.q, context)
       .await
       .map_err(LemmyError::from)
@@ -555,3 +578,142 @@ impl Perform for SaveSiteConfig {
     Ok(GetSiteConfigResponse { config_hjson })
   }
 }
+
+/// Lists registration applications, filterable by undenied only.
+#[async_trait::async_trait(?Send)]
+impl Perform for ListRegistrationApplications {
+  type Response = ListRegistrationApplicationsResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Make sure user is an admin
+    is_admin(&local_user_view)?;
+
+    let unread_only = data.unread_only;
+    let verified_email_only = blocking(context.pool(), Site::read_simple)
+      .await??
+      .require_email_verification;
+
+    let page = data.page;
+    let limit = data.limit;
+    let registration_applications = blocking(context.pool(), move |conn| {
+      RegistrationApplicationQueryBuilder::create(conn)
+        .unread_only(unread_only)
+        .verified_email_only(verified_email_only)
+        .page(page)
+        .limit(limit)
+        .list()
+    })
+    .await??;
+
+    let res = Self::Response {
+      registration_applications,
+    };
+
+    Ok(res)
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for ApproveRegistrationApplication {
+  type Response = RegistrationApplicationResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    let app_id = data.id;
+
+    // Only let admins do this
+    is_admin(&local_user_view)?;
+
+    // Update the registration with reason, admin_id
+    let deny_reason = diesel_option_overwrite(&data.deny_reason);
+    let app_form = RegistrationApplicationForm {
+      admin_id: Some(local_user_view.person.id),
+      deny_reason,
+      ..RegistrationApplicationForm::default()
+    };
+
+    let registration_application = blocking(context.pool(), move |conn| {
+      RegistrationApplication::update(conn, app_id, &app_form)
+    })
+    .await??;
+
+    // Update the local_user row
+    let local_user_form = LocalUserForm {
+      accepted_application: Some(data.approve),
+      ..LocalUserForm::default()
+    };
+
+    let approved_user_id = registration_application.local_user_id;
+    blocking(context.pool(), move |conn| {
+      LocalUser::update(conn, approved_user_id, &local_user_form)
+    })
+    .await??;
+
+    if data.approve {
+      let approved_local_user_view = blocking(context.pool(), move |conn| {
+        LocalUserView::read(conn, approved_user_id)
+      })
+      .await??;
+
+      if approved_local_user_view.local_user.email.is_some() {
+        send_application_approved_email(&approved_local_user_view, &context.settings())?;
+      }
+    }
+
+    // Read the view
+    let registration_application = blocking(context.pool(), move |conn| {
+      RegistrationApplicationView::read(conn, app_id)
+    })
+    .await??;
+
+    Ok(Self::Response {
+      registration_application,
+    })
+  }
+}
+
+#[async_trait::async_trait(?Send)]
+impl Perform for GetUnreadRegistrationApplicationCount {
+  type Response = GetUnreadRegistrationApplicationCountResponse;
+
+  async fn perform(
+    &self,
+    context: &Data<LemmyContext>,
+    _websocket_id: Option<ConnectionId>,
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
+    let local_user_view =
+      get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
+
+    // Only let admins do this
+    is_admin(&local_user_view)?;
+
+    let verified_email_only = blocking(context.pool(), Site::read_simple)
+      .await??
+      .require_email_verification;
+
+    let registration_applications = blocking(context.pool(), move |conn| {
+      RegistrationApplicationView::get_unread_count(conn, verified_email_only)
+    })
+    .await??;
+
+    Ok(Self::Response {
+      registration_applications,
+    })
+  }
+}
diff --git a/crates/api_common/Cargo.toml b/crates/api_common/Cargo.toml
index 1bac4e5c..602d10cb 100644
--- a/crates/api_common/Cargo.toml
+++ b/crates/api_common/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_api_common"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -13,11 +13,11 @@ path = "src/lib.rs"
 doctest = false
 
 [dependencies]
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
 serde = { version = "1.0.131", features = ["derive"] }
 diesel = "1.4.8"
 actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
diff --git a/crates/api_common/src/lib.rs b/crates/api_common/src/lib.rs
index 29e26e1e..cd854a6d 100644
--- a/crates/api_common/src/lib.rs
+++ b/crates/api_common/src/lib.rs
@@ -10,8 +10,11 @@ use lemmy_db_schema::{
   newtypes::{CommunityId, LocalUserId, PersonId, PostId},
   source::{
     community::Community,
+    email_verification::{EmailVerification, EmailVerificationForm},
+    password_reset_request::PasswordResetRequest,
     person_block::PersonBlock,
     post::{Post, PostRead, PostReadForm},
+    registration_application::RegistrationApplication,
     secret::Secret,
     site::Site,
   },
@@ -23,7 +26,14 @@ use lemmy_db_views_actor::{
   community_person_ban_view::CommunityPersonBanView,
   community_view::CommunityView,
 };
-use lemmy_utils::{claims::Claims, settings::structs::FederationConfig, LemmyError, Sensitive};
+use lemmy_utils::{
+  claims::Claims,
+  email::send_email,
+  settings::structs::{FederationConfig, Settings},
+  utils::generate_random_string,
+  LemmyError,
+  Sensitive,
+};
 use url::Url;
 
 pub async fn blocking<F, T>(pool: &DbPool, f: F) -> Result<T, LemmyError>
@@ -252,6 +262,19 @@ pub async fn check_downvotes_enabled(score: i16, pool: &DbPool) -> Result<(), Le
   Ok(())
 }
 
+pub async fn check_private_instance(
+  local_user_view: &Option<LocalUserView>,
+  pool: &DbPool,
+) -> Result<(), LemmyError> {
+  if local_user_view.is_none() {
+    let site = blocking(pool, Site::read_simple).await??;
+    if site.private_instance {
+      return Err(LemmyError::from_message("instance_is_private"));
+    }
+  }
+  Ok(())
+}
+
 pub async fn build_federated_instances(
   pool: &DbPool,
   federation_config: &FederationConfig,
@@ -320,3 +343,163 @@ pub fn honeypot_check(honeypot: &Option<String>) -> Result<(), LemmyError> {
     Ok(())
   }
 }
+
+pub fn send_email_to_user(
+  local_user_view: &LocalUserView,
+  subject_text: &str,
+  body_text: &str,
+  comment_content: &str,
+  settings: &Settings,
+) {
+  if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
+    return;
+  }
+
+  if let Some(user_email) = &local_user_view.local_user.email {
+    let subject = &format!(
+      "{} - {} {}",
+      subject_text, settings.hostname, local_user_view.person.name,
+    );
+    let html = &format!(
+      "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+      body_text,
+      local_user_view.person.name,
+      comment_content,
+      settings.get_protocol_and_hostname()
+    );
+    match send_email(
+      subject,
+      user_email,
+      &local_user_view.person.name,
+      html,
+      settings,
+    ) {
+      Ok(_o) => _o,
+      Err(e) => tracing::error!("{}", e),
+    };
+  }
+}
+
+pub async fn send_password_reset_email(
+  local_user_view: &LocalUserView,
+  pool: &DbPool,
+  settings: &Settings,
+) -> Result<(), LemmyError> {
+  // Generate a random token
+  let token = generate_random_string();
+
+  // Insert the row
+  let token2 = token.clone();
+  let local_user_id = local_user_view.local_user.id;
+  blocking(pool, move |conn| {
+    PasswordResetRequest::create_token(conn, local_user_id, &token2)
+  })
+  .await??;
+
+  let email = &local_user_view.local_user.email.to_owned().expect("email");
+  let subject = &format!("Password reset for {}", local_user_view.person.name);
+  let protocol_and_hostname = settings.get_protocol_and_hostname();
+  let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", local_user_view.person.name, protocol_and_hostname, &token);
+  send_email(subject, email, &local_user_view.person.name, html, settings)
+}
+
+/// Send a verification email
+pub async fn send_verification_email(
+  local_user_id: LocalUserId,
+  new_email: &str,
+  username: &str,
+  pool: &DbPool,
+  settings: &Settings,
+) -> Result<(), LemmyError> {
+  let form = EmailVerificationForm {
+    local_user_id,
+    email: new_email.to_string(),
+    verification_token: generate_random_string(),
+  };
+  let verify_link = format!(
+    "{}/verify_email/{}",
+    settings.get_protocol_and_hostname(),
+    &form.verification_token
+  );
+  blocking(pool, move |conn| EmailVerification::create(conn, &form)).await??;
+
+  let subject = format!("Verify your email address for {}", settings.hostname);
+  let body = format!(
+    concat!(
+      "Please click the link below to verify your email address ",
+      "for the account @{}@{}. Ignore this email if the account isn't yours.<br><br>",
+      "<a href=\"{}\">Verify your email</a>"
+    ),
+    username, settings.hostname, verify_link
+  );
+  send_email(&subject, new_email, username, &body, settings)?;
+
+  Ok(())
+}
+
+pub fn send_email_verification_success(
+  local_user_view: &LocalUserView,
+  settings: &Settings,
+) -> Result<(), LemmyError> {
+  let email = &local_user_view.local_user.email.to_owned().expect("email");
+  let subject = &format!("Email verified for {}", local_user_view.person.actor_id);
+  let html = "Your email has been verified.";
+  send_email(subject, email, &local_user_view.person.name, html, settings)
+}
+
+pub fn send_application_approved_email(
+  local_user_view: &LocalUserView,
+  settings: &Settings,
+) -> Result<(), LemmyError> {
+  let email = &local_user_view.local_user.email.to_owned().expect("email");
+  let subject = &format!(
+    "Registration approved for {}",
+    local_user_view.person.actor_id
+  );
+  let html = &format!(
+    "Your registration application has been approved. Welcome to {}!",
+    settings.hostname
+  );
+  send_email(subject, email, &local_user_view.person.name, html, settings)
+}
+
+pub async fn check_registration_application(
+  site: &Site,
+  local_user_view: &LocalUserView,
+  pool: &DbPool,
+) -> Result<(), LemmyError> {
+  if site.require_application
+    && !local_user_view.local_user.accepted_application
+    && !local_user_view.person.admin
+  {
+    // Fetch the registration, see if its denied
+    let local_user_id = local_user_view.local_user.id;
+    let registration = blocking(pool, move |conn| {
+      RegistrationApplication::find_by_local_user_id(conn, local_user_id)
+    })
+    .await??;
+    if registration.deny_reason.is_some() {
+      return Err(LemmyError::from_message("registration_denied"));
+    } else {
+      return Err(LemmyError::from_message("registration_application_pending"));
+    }
+  }
+  Ok(())
+}
+
+/// TODO this check should be removed after https://github.com/LemmyNet/lemmy/issues/868 is done.
+pub async fn check_private_instance_and_federation_enabled(
+  pool: &DbPool,
+  settings: &Settings,
+) -> Result<(), LemmyError> {
+  let site_opt = blocking(pool, Site::read_simple).await?;
+
+  if let Ok(site) = site_opt {
+    if site.private_instance && settings.federation.enabled {
+      return Err(LemmyError::from_message(
+        "Cannot have both private instance and federation enabled.",
+      ));
+    }
+  }
+  Ok(())
+}
diff --git a/crates/api_common/src/person.rs b/crates/api_common/src/person.rs
index 5dddc5dc..47c26591 100644
--- a/crates/api_common/src/person.rs
+++ b/crates/api_common/src/person.rs
@@ -24,10 +24,13 @@ pub struct Register {
   pub password: Sensitive<String>,
   pub password_verify: Sensitive<String>,
   pub show_nsfw: bool,
+  /// email is mandatory if email verification is enabled on the server
   pub email: Option<Sensitive<String>>,
   pub captcha_uuid: Option<String>,
   pub captcha_answer: Option<String>,
   pub honeypot: Option<String>,
+  /// An answer is mandatory if require application is enabled on the server
+  pub answer: Option<String>,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -78,7 +81,10 @@ pub struct ChangePassword {
 
 #[derive(Debug, Serialize, Deserialize)]
 pub struct LoginResponse {
-  pub jwt: Sensitive<String>,
+  /// This is None in response to `Register` if email verification is enabled, or the server requires registration applications.
+  pub jwt: Option<Sensitive<String>>,
+  pub registration_created: bool,
+  pub verify_email_sent: bool,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -194,6 +200,9 @@ pub struct DeleteAccount {
   pub auth: Sensitive<String>,
 }
 
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct DeleteAccountResponse {}
+
 #[derive(Debug, Serialize, Deserialize)]
 pub struct PasswordReset {
   pub email: Sensitive<String>,
@@ -279,3 +288,11 @@ pub struct GetUnreadCountResponse {
   pub mentions: i64,
   pub private_messages: i64,
 }
+
+#[derive(Serialize, Deserialize)]
+pub struct VerifyEmail {
+  pub token: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct VerifyEmailResponse {}
diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs
index ff53cb51..b53b99d4 100644
--- a/crates/api_common/src/site.rs
+++ b/crates/api_common/src/site.rs
@@ -3,6 +3,7 @@ use lemmy_db_views::{
   comment_view::CommentView,
   local_user_view::LocalUserSettingsView,
   post_view::PostView,
+  registration_application_view::RegistrationApplicationView,
   site_view::SiteView,
 };
 use lemmy_db_views_actor::{
@@ -71,6 +72,7 @@ pub struct GetModlog {
   pub community_id: Option<CommunityId>,
   pub page: Option<i64>,
   pub limit: Option<i64>,
+  pub auth: Option<Sensitive<String>>,
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -98,6 +100,10 @@ pub struct CreateSite {
   pub open_registration: Option<bool>,
   pub enable_nsfw: Option<bool>,
   pub community_creation_admin_only: Option<bool>,
+  pub require_email_verification: Option<bool>,
+  pub require_application: Option<bool>,
+  pub application_question: Option<String>,
+  pub private_instance: Option<bool>,
   pub auth: Sensitive<String>,
 }
 
@@ -112,6 +118,10 @@ pub struct EditSite {
   pub open_registration: Option<bool>,
   pub enable_nsfw: Option<bool>,
   pub community_creation_admin_only: Option<bool>,
+  pub require_email_verification: Option<bool>,
+  pub require_application: Option<bool>,
+  pub application_question: Option<String>,
+  pub private_instance: Option<bool>,
   pub auth: Sensitive<String>,
 }
 
@@ -173,3 +183,40 @@ pub struct FederatedInstances {
   pub allowed: Option<Vec<String>>,
   pub blocked: Option<Vec<String>>,
 }
+
+#[derive(Serialize, Deserialize)]
+pub struct ListRegistrationApplications {
+  /// Only shows the unread applications (IE those without an admin actor)
+  pub unread_only: Option<bool>,
+  pub page: Option<i64>,
+  pub limit: Option<i64>,
+  pub auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ListRegistrationApplicationsResponse {
+  pub registration_applications: Vec<RegistrationApplicationView>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ApproveRegistrationApplication {
+  pub id: i32,
+  pub approve: bool,
+  pub deny_reason: Option<String>,
+  pub auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct RegistrationApplicationResponse {
+  pub registration_application: RegistrationApplicationView,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetUnreadRegistrationApplicationCount {
+  pub auth: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct GetUnreadRegistrationApplicationCountResponse {
+  pub registration_applications: i64,
+}
diff --git a/crates/api_crud/Cargo.toml b/crates/api_crud/Cargo.toml
index cbbbe4b0..659385cd 100644
--- a/crates/api_crud/Cargo.toml
+++ b/crates/api_crud/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_api_crud"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -8,15 +8,15 @@ homepage = "https://join-lemmy.org/"
 documentation = "https://join-lemmy.org/docs/en/index.html"
 
 [dependencies]
-lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_moderator = { version = "=0.14.4-rc.4", path = "../db_views_moderator" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_moderator = { version = "=0.15.0-rc.6", path = "../db_views_moderator" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
 diesel = "1.4.8"
 bcrypt = "0.10.1"
 chrono = { version = "0.4.19", features = ["serde"] }
diff --git a/crates/api_crud/src/comment/read.rs b/crates/api_crud/src/comment/read.rs
index bd1f475f..45946991 100644
--- a/crates/api_crud/src/comment/read.rs
+++ b/crates/api_crud/src/comment/read.rs
@@ -1,6 +1,11 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
-use lemmy_api_common::{blocking, comment::*, get_local_user_view_from_jwt_opt};
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  comment::*,
+  get_local_user_view_from_jwt_opt,
+};
 use lemmy_apub::{
   fetcher::webfinger::webfinger_resolve,
   objects::community::ApubCommunity,
@@ -31,6 +36,8 @@ impl PerformCrud for GetComment {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let person_id = local_user_view.map(|u| u.person.id);
     let id = data.id;
     let comment_view = blocking(context.pool(), move |conn| {
@@ -63,6 +70,8 @@ impl PerformCrud for GetComments {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let show_bot_accounts = local_user_view
       .as_ref()
       .map(|t| t.local_user.show_bot_accounts);
diff --git a/crates/api_crud/src/community/read.rs b/crates/api_crud/src/community/read.rs
index 32ccf73d..2ec4054b 100644
--- a/crates/api_crud/src/community/read.rs
+++ b/crates/api_crud/src/community/read.rs
@@ -1,6 +1,11 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
-use lemmy_api_common::{blocking, community::*, get_local_user_view_from_jwt_opt};
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  community::*,
+  get_local_user_view_from_jwt_opt,
+};
 use lemmy_apub::{
   fetcher::webfinger::webfinger_resolve,
   objects::community::ApubCommunity,
@@ -34,6 +39,9 @@ impl PerformCrud for GetCommunity {
     let local_user_view =
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
+
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let person_id = local_user_view.map(|u| u.person.id);
 
     let community_id = match data.id {
@@ -105,6 +113,8 @@ impl PerformCrud for ListCommunities {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let person_id = local_user_view.to_owned().map(|l| l.person.id);
 
     // Don't show NSFW by default
diff --git a/crates/api_crud/src/post/read.rs b/crates/api_crud/src/post/read.rs
index 42e84254..10ecefc3 100644
--- a/crates/api_crud/src/post/read.rs
+++ b/crates/api_crud/src/post/read.rs
@@ -1,6 +1,12 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
-use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, mark_post_as_read, post::*};
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  get_local_user_view_from_jwt_opt,
+  mark_post_as_read,
+  post::*,
+};
 use lemmy_apub::{
   fetcher::webfinger::webfinger_resolve,
   objects::community::ApubCommunity,
@@ -38,6 +44,8 @@ impl PerformCrud for GetPost {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let show_bot_accounts = local_user_view
       .as_ref()
       .map(|t| t.local_user.show_bot_accounts);
@@ -130,6 +138,8 @@ impl PerformCrud for GetPosts {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let person_id = local_user_view.to_owned().map(|l| l.person.id);
 
     let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
diff --git a/crates/api_crud/src/private_message/create.rs b/crates/api_crud/src/private_message/create.rs
index ae75d9ea..54edc240 100644
--- a/crates/api_crud/src/private_message/create.rs
+++ b/crates/api_crud/src/private_message/create.rs
@@ -5,6 +5,7 @@ use lemmy_api_common::{
   check_person_block,
   get_local_user_view_from_jwt,
   person::{CreatePrivateMessage, PrivateMessageResponse},
+  send_email_to_user,
 };
 use lemmy_apub::{
   generate_local_apub_endpoint,
@@ -20,11 +21,7 @@ use lemmy_db_schema::{
 };
 use lemmy_db_views::local_user_view::LocalUserView;
 use lemmy_utils::{utils::remove_slurs, ConnectionId, LemmyError};
-use lemmy_websocket::{
-  send::{send_email_to_user, send_pm_ws_message},
-  LemmyContext,
-  UserOperationCrud,
-};
+use lemmy_websocket::{send::send_pm_ws_message, LemmyContext, UserOperationCrud};
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for CreatePrivateMessage {
diff --git a/crates/api_crud/src/site/create.rs b/crates/api_crud/src/site/create.rs
index f638eb7f..043a31c2 100644
--- a/crates/api_crud/src/site/create.rs
+++ b/crates/api_crud/src/site/create.rs
@@ -66,8 +66,8 @@ impl PerformCrud for CreateSite {
       enable_downvotes: data.enable_downvotes,
       open_registration: data.open_registration,
       enable_nsfw: data.enable_nsfw,
-      updated: None,
       community_creation_admin_only: data.community_creation_admin_only,
+      ..SiteForm::default()
     };
 
     let create_site = move |conn: &'_ _| Site::create(conn, &site_form);
diff --git a/crates/api_crud/src/site/read.rs b/crates/api_crud/src/site/read.rs
index 4410d66a..06146b96 100644
--- a/crates/api_crud/src/site/read.rs
+++ b/crates/api_crud/src/site/read.rs
@@ -45,8 +45,13 @@ impl PerformCrud for GetSite {
             captcha_uuid: None,
             captcha_answer: None,
             honeypot: None,
+            answer: None,
           };
-          let login_response = register.perform(context, websocket_id).await?;
+          let admin_jwt = register
+            .perform(context, websocket_id)
+            .await?
+            .jwt
+            .expect("jwt is returned from registration on newly created site");
           info!("Admin {} created", setup.admin_username);
 
           let create_site = CreateSite {
@@ -59,7 +64,11 @@ impl PerformCrud for GetSite {
             open_registration: setup.open_registration,
             enable_nsfw: setup.enable_nsfw,
             community_creation_admin_only: setup.community_creation_admin_only,
-            auth: login_response.jwt,
+            require_email_verification: setup.require_email_verification,
+            require_application: setup.require_application,
+            application_question: setup.application_question.to_owned(),
+            private_instance: setup.private_instance,
+            auth: admin_jwt,
           };
           create_site.perform(context, websocket_id).await?;
           info!("Site {} created", setup.site_name);
diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs
index 21dc50a2..bcd6a1a3 100644
--- a/crates/api_crud/src/site/update.rs
+++ b/crates/api_crud/src/site/update.rs
@@ -11,7 +11,10 @@ use lemmy_db_schema::{
   diesel_option_overwrite,
   diesel_option_overwrite_to_url,
   naive_now,
-  source::site::{Site, SiteForm},
+  source::{
+    local_user::LocalUser,
+    site::{Site, SiteForm},
+  },
   traits::Crud,
 };
 use lemmy_db_views::site_view::SiteView;
@@ -42,6 +45,7 @@ impl PerformCrud for EditSite {
 
     let sidebar = diesel_option_overwrite(&data.sidebar);
     let description = diesel_option_overwrite(&data.description);
+    let application_question = diesel_option_overwrite(&data.application_question);
     let icon = diesel_option_overwrite_to_url(&data.icon)?;
     let banner = diesel_option_overwrite_to_url(&data.banner)?;
 
@@ -61,13 +65,41 @@ impl PerformCrud for EditSite {
       open_registration: data.open_registration,
       enable_nsfw: data.enable_nsfw,
       community_creation_admin_only: data.community_creation_admin_only,
+      require_email_verification: data.require_email_verification,
+      require_application: data.require_application,
+      application_question,
+      private_instance: data.private_instance,
     };
 
-    let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
-    blocking(context.pool(), update_site)
+    let update_site = blocking(context.pool(), move |conn| {
+      Site::update(conn, 1, &site_form)
+    })
+    .await?
+    .map_err(LemmyError::from)
+    .map_err(|e| e.with_message("couldnt_update_site"))?;
+
+    // TODO can't think of a better way to do this.
+    // If the server suddenly requires email verification, or required applications, no old users
+    // will be able to log in. It really only wants this to be a requirement for NEW signups.
+    // So if it was set from false, to true, you need to update all current users columns to be verified.
+
+    if !found_site.require_application && update_site.require_application {
+      blocking(context.pool(), move |conn| {
+        LocalUser::set_all_users_registration_applications_accepted(conn)
+      })
       .await?
       .map_err(LemmyError::from)
-      .map_err(|e| e.with_message("couldnt_update_site"))?;
+      .map_err(|e| e.with_message("couldnt_set_all_registrations_accepted"))?;
+    }
+
+    if !found_site.require_email_verification && update_site.require_email_verification {
+      blocking(context.pool(), move |conn| {
+        LocalUser::set_all_users_email_verified(conn)
+      })
+      .await?
+      .map_err(LemmyError::from)
+      .map_err(|e| e.with_message("couldnt_set_all_email_verified"))?;
+    }
 
     let site_view = blocking(context.pool(), SiteView::read).await??;
 
diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs
index a9ea9861..6f8a5fa0 100644
--- a/crates/api_crud/src/user/create.rs
+++ b/crates/api_crud/src/user/create.rs
@@ -1,6 +1,12 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
-use lemmy_api_common::{blocking, honeypot_check, password_length_check, person::*};
+use lemmy_api_common::{
+  blocking,
+  honeypot_check,
+  password_length_check,
+  person::*,
+  send_verification_email,
+};
 use lemmy_apub::{
   generate_followers_url,
   generate_inbox_url,
@@ -21,11 +27,10 @@ use lemmy_db_schema::{
     },
     local_user::{LocalUser, LocalUserForm},
     person::{Person, PersonForm},
+    registration_application::{RegistrationApplication, RegistrationApplicationForm},
     site::Site,
   },
   traits::{Crud, Followable, Joinable},
-  ListingType,
-  SortType,
 };
 use lemmy_db_views_actor::person_view::PersonViewSafe;
 use lemmy_utils::{
@@ -49,16 +54,31 @@ impl PerformCrud for Register {
   ) -> Result<LoginResponse, LemmyError> {
     let data: &Register = self;
 
+    // no email verification, or applications if the site is not setup yet
+    let (mut email_verification, mut require_application) = (false, false);
+
     // Make sure site has open registration
     if let Ok(site) = blocking(context.pool(), Site::read_simple).await? {
       if !site.open_registration {
         return Err(LemmyError::from_message("registration_closed"));
       }
+      email_verification = site.require_email_verification;
+      require_application = site.require_application;
     }
 
     password_length_check(&data.password)?;
     honeypot_check(&data.honeypot)?;
 
+    if email_verification && data.email.is_none() {
+      return Err(LemmyError::from_message("email_required"));
+    }
+
+    if require_application && data.answer.is_none() {
+      return Err(LemmyError::from_message(
+        "registration_application_answer_required",
+      ));
+    }
+
     // Make sure passwords match
     if data.password != data.password_verify {
       return Err(LemmyError::from_message("passwords_dont_match"));
@@ -125,22 +145,13 @@ impl PerformCrud for Register {
     .map_err(|e| e.with_message("user_already_exists"))?;
 
     // Create the local user
-    // TODO some of these could probably use the DB defaults
     let local_user_form = LocalUserForm {
-      person_id: inserted_person.id,
+      person_id: Some(inserted_person.id),
       email: Some(data.email.as_deref().map(|s| s.to_owned())),
-      password_encrypted: data.password.to_string(),
+      password_encrypted: Some(data.password.to_string()),
       show_nsfw: Some(data.show_nsfw),
-      show_bot_accounts: Some(true),
-      theme: Some("browser".into()),
-      default_sort_type: Some(SortType::Active as i16),
-      default_listing_type: Some(ListingType::Subscribed as i16),
-      lang: Some("browser".into()),
-      show_avatars: Some(true),
-      show_scores: Some(true),
-      show_read_posts: Some(true),
-      show_new_post_notifs: Some(false),
-      send_notifications_to_email: Some(false),
+      email_verified: Some(false),
+      ..LocalUserForm::default()
     };
 
     let inserted_local_user = match blocking(context.pool(), move |conn| {
@@ -168,6 +179,21 @@ impl PerformCrud for Register {
       }
     };
 
+    if require_application {
+      // Create the registration application
+      let form = RegistrationApplicationForm {
+        local_user_id: Some(inserted_local_user.id),
+        // We already made sure answer was not null above
+        answer: data.answer.to_owned(),
+        ..RegistrationApplicationForm::default()
+      };
+
+      blocking(context.pool(), move |conn| {
+        RegistrationApplication::create(conn, &form)
+      })
+      .await??;
+    }
+
     let main_community_keypair = generate_actor_keypair()?;
 
     // Create the main community if it doesn't exist
@@ -231,14 +257,41 @@ impl PerformCrud for Register {
         .map_err(|e| e.with_message("community_moderator_already_exists"))?;
     }
 
-    // Return the jwt
-    Ok(LoginResponse {
-      jwt: Claims::jwt(
-        inserted_local_user.id.0,
-        &context.secret().jwt_secret,
-        &context.settings().hostname,
-      )?
-      .into(),
-    })
+    let mut login_response = LoginResponse {
+      jwt: None,
+      registration_created: false,
+      verify_email_sent: false,
+    };
+
+    // Log the user in directly if email verification and application aren't required
+    if !require_application && !email_verification {
+      login_response.jwt = Some(
+        Claims::jwt(
+          inserted_local_user.id.0,
+          &context.secret().jwt_secret,
+          &context.settings().hostname,
+        )?
+        .into(),
+      );
+    } else {
+      if email_verification {
+        send_verification_email(
+          inserted_local_user.id,
+          // we check at the beginning of this method that email is set
+          &inserted_local_user.email.expect("email was provided"),
+          &inserted_person.name,
+          context.pool(),
+          &context.settings(),
+        )
+        .await?;
+        login_response.verify_email_sent = true;
+      }
+
+      if require_application {
+        login_response.registration_created = true;
+      }
+    }
+
+    Ok(login_response)
   }
 }
diff --git a/crates/api_crud/src/user/delete.rs b/crates/api_crud/src/user/delete.rs
index 0400ffe0..c3977e72 100644
--- a/crates/api_crud/src/user/delete.rs
+++ b/crates/api_crud/src/user/delete.rs
@@ -8,15 +8,15 @@ use lemmy_websocket::LemmyContext;
 
 #[async_trait::async_trait(?Send)]
 impl PerformCrud for DeleteAccount {
-  type Response = LoginResponse;
+  type Response = DeleteAccountResponse;
 
   #[tracing::instrument(skip(self, context, _websocket_id))]
   async fn perform(
     &self,
     context: &Data<LemmyContext>,
     _websocket_id: Option<ConnectionId>,
-  ) -> Result<LoginResponse, LemmyError> {
-    let data: &DeleteAccount = self;
+  ) -> Result<Self::Response, LemmyError> {
+    let data = self;
     let local_user_view =
       get_local_user_view_from_jwt(data.auth.as_ref(), context.pool(), context.secret()).await?;
 
@@ -50,8 +50,6 @@ impl PerformCrud for DeleteAccount {
     })
     .await??;
 
-    Ok(LoginResponse {
-      jwt: data.auth.clone(),
-    })
+    Ok(DeleteAccountResponse {})
   }
 }
diff --git a/crates/api_crud/src/user/read.rs b/crates/api_crud/src/user/read.rs
index 20864ab6..efa058b1 100644
--- a/crates/api_crud/src/user/read.rs
+++ b/crates/api_crud/src/user/read.rs
@@ -1,6 +1,11 @@
 use crate::PerformCrud;
 use actix_web::web::Data;
-use lemmy_api_common::{blocking, get_local_user_view_from_jwt_opt, person::*};
+use lemmy_api_common::{
+  blocking,
+  check_private_instance,
+  get_local_user_view_from_jwt_opt,
+  person::*,
+};
 use lemmy_apub::{
   fetcher::webfinger::webfinger_resolve,
   objects::person::ApubPerson,
@@ -31,6 +36,8 @@ impl PerformCrud for GetPersonDetails {
       get_local_user_view_from_jwt_opt(data.auth.as_ref(), context.pool(), context.secret())
         .await?;
 
+    check_private_instance(&local_user_view, context.pool()).await?;
+
     let show_nsfw = local_user_view.as_ref().map(|t| t.local_user.show_nsfw);
     let show_bot_accounts = local_user_view
       .as_ref()
diff --git a/crates/apub/Cargo.toml b/crates/apub/Cargo.toml
index ac155244..d02a4134 100644
--- a/crates/apub/Cargo.toml
+++ b/crates/apub/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_apub"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -13,13 +13,13 @@ path = "src/lib.rs"
 doctest = false
 
 [dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
 diesel = "1.4.8"
 activitystreams-kinds = "0.1.2"
 bcrypt = "0.10.1"
diff --git a/crates/apub_lib/Cargo.toml b/crates/apub_lib/Cargo.toml
index c65821a5..a46d3c35 100644
--- a/crates/apub_lib/Cargo.toml
+++ b/crates/apub_lib/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_apub_lib"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -8,8 +8,8 @@ homepage = "https://join-lemmy.org/"
 documentation = "https://join-lemmy.org/docs/en/index.html"
 
 [dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_apub_lib_derive = { version = "=0.14.4-rc.4", path = "../apub_lib_derive" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_apub_lib_derive = { version = "=0.15.0-rc.6", path = "../apub_lib_derive" }
 activitystreams = "0.7.0-alpha.14"
 serde = { version = "1.0.131", features = ["derive"] }
 async-trait = "0.1.52"
diff --git a/crates/apub_lib_derive/Cargo.toml b/crates/apub_lib_derive/Cargo.toml
index c0c47219..2b2faf2c 100644
--- a/crates/apub_lib_derive/Cargo.toml
+++ b/crates/apub_lib_derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_apub_lib_derive"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
diff --git a/crates/db_schema/Cargo.toml b/crates/db_schema/Cargo.toml
index 21e4a1b9..e6b795e9 100644
--- a/crates/db_schema/Cargo.toml
+++ b/crates/db_schema/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_db_schema"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -11,8 +11,8 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
 doctest = false
 
 [dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_apub_lib = { version = "=0.14.4-rc.4", path = "../apub_lib" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_apub_lib = { version = "=0.15.0-rc.6", path = "../apub_lib" }
 diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
 diesel_migrations = "1.4.0"
 chrono = { version = "0.4.19", features = ["serde"] }
diff --git a/crates/db_schema/src/aggregates/site_aggregates.rs b/crates/db_schema/src/aggregates/site_aggregates.rs
index 4b4b6c43..08d4dd01 100644
--- a/crates/db_schema/src/aggregates/site_aggregates.rs
+++ b/crates/db_schema/src/aggregates/site_aggregates.rs
@@ -65,6 +65,10 @@ mod tests {
       enable_nsfw: None,
       updated: None,
       community_creation_admin_only: Some(false),
+      require_email_verification: None,
+      require_application: None,
+      application_question: None,
+      private_instance: None,
     };
 
     Site::create(&conn, &site_form).unwrap();
diff --git a/crates/db_schema/src/impls/email_verification.rs b/crates/db_schema/src/impls/email_verification.rs
new file mode 100644
index 00000000..c2703739
--- /dev/null
+++ b/crates/db_schema/src/impls/email_verification.rs
@@ -0,0 +1,55 @@
+use crate::{newtypes::LocalUserId, source::email_verification::*, traits::Crud};
+use diesel::{
+  dsl::*,
+  insert_into,
+  result::Error,
+  ExpressionMethods,
+  PgConnection,
+  QueryDsl,
+  RunQueryDsl,
+};
+
+impl Crud for EmailVerification {
+  type Form = EmailVerificationForm;
+  type IdType = i32;
+  fn create(conn: &PgConnection, form: &EmailVerificationForm) -> Result<Self, Error> {
+    use crate::schema::email_verification::dsl::*;
+    insert_into(email_verification)
+      .values(form)
+      .get_result::<Self>(conn)
+  }
+
+  fn read(conn: &PgConnection, id_: i32) -> Result<Self, Error> {
+    use crate::schema::email_verification::dsl::*;
+    email_verification.find(id_).first::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, id_: i32, form: &EmailVerificationForm) -> Result<Self, Error> {
+    use crate::schema::email_verification::dsl::*;
+    diesel::update(email_verification.find(id_))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, id_: i32) -> Result<usize, Error> {
+    use crate::schema::email_verification::dsl::*;
+    diesel::delete(email_verification.find(id_)).execute(conn)
+  }
+}
+
+impl EmailVerification {
+  pub fn read_for_token(conn: &PgConnection, token: &str) -> Result<Self, Error> {
+    use crate::schema::email_verification::dsl::*;
+    email_verification
+      .filter(verification_token.eq(token))
+      .filter(published.gt(now - 7.days()))
+      .first::<Self>(conn)
+  }
+  pub fn delete_old_tokens_for_local_user(
+    conn: &PgConnection,
+    local_user_id_: LocalUserId,
+  ) -> Result<usize, Error> {
+    use crate::schema::email_verification::dsl::*;
+    diesel::delete(email_verification.filter(local_user_id.eq(local_user_id_))).execute(conn)
+  }
+}
diff --git a/crates/db_schema/src/impls/local_user.rs b/crates/db_schema/src/impls/local_user.rs
index 3a2d5769..833d6bdb 100644
--- a/crates/db_schema/src/impls/local_user.rs
+++ b/crates/db_schema/src/impls/local_user.rs
@@ -31,6 +31,8 @@ mod safe_settings_type {
     show_scores,
     show_read_posts,
     show_new_post_notifs,
+    email_verified,
+    accepted_application,
   );
 
   impl ToSafeSettings for LocalUser {
@@ -54,6 +56,8 @@ mod safe_settings_type {
         show_scores,
         show_read_posts,
         show_new_post_notifs,
+        email_verified,
+        accepted_application,
       )
     }
   }
@@ -62,8 +66,10 @@ mod safe_settings_type {
 impl LocalUser {
   pub fn register(conn: &PgConnection, form: &LocalUserForm) -> Result<Self, Error> {
     let mut edited_user = form.clone();
-    let password_hash =
-      hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
+    let password_hash = form
+      .password_encrypted
+      .as_ref()
+      .map(|p| hash(p, DEFAULT_COST).expect("Couldn't hash password"));
     edited_user.password_encrypted = password_hash;
 
     Self::create(conn, &edited_user)
@@ -83,6 +89,20 @@ impl LocalUser {
       ))
       .get_result::<Self>(conn)
   }
+
+  pub fn set_all_users_email_verified(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+    diesel::update(local_user)
+      .set(email_verified.eq(true))
+      .get_results::<Self>(conn)
+  }
+
+  pub fn set_all_users_registration_applications_accepted(
+    conn: &PgConnection,
+  ) -> Result<Vec<Self>, Error> {
+    diesel::update(local_user)
+      .set(accepted_application.eq(true))
+      .get_results::<Self>(conn)
+  }
 }
 
 impl Crud for LocalUser {
diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs
index a1e45efa..c96e3a62 100644
--- a/crates/db_schema/src/impls/mod.rs
+++ b/crates/db_schema/src/impls/mod.rs
@@ -3,6 +3,7 @@ pub mod comment;
 pub mod comment_report;
 pub mod community;
 pub mod community_block;
+pub mod email_verification;
 pub mod local_user;
 pub mod moderator;
 pub mod password_reset_request;
@@ -12,5 +13,6 @@ pub mod person_mention;
 pub mod post;
 pub mod post_report;
 pub mod private_message;
+pub mod registration_application;
 pub mod secret;
 pub mod site;
diff --git a/crates/db_schema/src/impls/password_reset_request.rs b/crates/db_schema/src/impls/password_reset_request.rs
index c5debd92..808f0ac0 100644
--- a/crates/db_schema/src/impls/password_reset_request.rs
+++ b/crates/db_schema/src/impls/password_reset_request.rs
@@ -93,8 +93,8 @@ mod tests {
     let inserted_person = Person::create(&conn, &new_person).unwrap();
 
     let new_local_user = LocalUserForm {
-      person_id: inserted_person.id,
-      password_encrypted: "pass".to_string(),
+      person_id: Some(inserted_person.id),
+      password_encrypted: Some("pass".to_string()),
       ..LocalUserForm::default()
     };
 
diff --git a/crates/db_schema/src/impls/registration_application.rs b/crates/db_schema/src/impls/registration_application.rs
new file mode 100644
index 00000000..5147dbb7
--- /dev/null
+++ b/crates/db_schema/src/impls/registration_application.rs
@@ -0,0 +1,42 @@
+use crate::{newtypes::LocalUserId, source::registration_application::*, traits::Crud};
+use diesel::{insert_into, result::Error, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl};
+
+impl Crud for RegistrationApplication {
+  type Form = RegistrationApplicationForm;
+  type IdType = i32;
+  fn create(conn: &PgConnection, form: &Self::Form) -> Result<Self, Error> {
+    use crate::schema::registration_application::dsl::*;
+    insert_into(registration_application)
+      .values(form)
+      .get_result::<Self>(conn)
+  }
+
+  fn read(conn: &PgConnection, id_: Self::IdType) -> Result<Self, Error> {
+    use crate::schema::registration_application::dsl::*;
+    registration_application.find(id_).first::<Self>(conn)
+  }
+
+  fn update(conn: &PgConnection, id_: Self::IdType, form: &Self::Form) -> Result<Self, Error> {
+    use crate::schema::registration_application::dsl::*;
+    diesel::update(registration_application.find(id_))
+      .set(form)
+      .get_result::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, id_: Self::IdType) -> Result<usize, Error> {
+    use crate::schema::registration_application::dsl::*;
+    diesel::delete(registration_application.find(id_)).execute(conn)
+  }
+}
+
+impl RegistrationApplication {
+  pub fn find_by_local_user_id(
+    conn: &PgConnection,
+    local_user_id_: LocalUserId,
+  ) -> Result<Self, Error> {
+    use crate::schema::registration_application::dsl::*;
+    registration_application
+      .filter(local_user_id.eq(local_user_id_))
+      .first::<Self>(conn)
+  }
+}
diff --git a/crates/db_schema/src/newtypes.rs b/crates/db_schema/src/newtypes.rs
index 9219d77f..b863a250 100644
--- a/crates/db_schema/src/newtypes.rs
+++ b/crates/db_schema/src/newtypes.rs
@@ -43,7 +43,9 @@ impl fmt::Display for CommentId {
 )]
 pub struct CommunityId(pub i32);
 
-#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
+#[derive(
+  Debug, Copy, Clone, Hash, Eq, PartialEq, Default, Serialize, Deserialize, DieselNewType,
+)]
 pub struct LocalUserId(pub i32);
 
 #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, DieselNewType)]
diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs
index 034edbe9..89ba5a11 100644
--- a/crates/db_schema/src/schema.rs
+++ b/crates/db_schema/src/schema.rs
@@ -157,6 +157,8 @@ table! {
         show_scores -> Bool,
         show_read_posts -> Bool,
         show_new_post_notifs -> Bool,
+        email_verified -> Bool,
+        accepted_application -> Bool,
     }
 }
 
@@ -447,6 +449,10 @@ table! {
         banner -> Nullable<Varchar>,
         description -> Nullable<Text>,
         community_creation_admin_only -> Bool,
+        require_email_verification -> Bool,
+        require_application -> Bool,
+        application_question -> Nullable<Text>,
+        private_instance -> Bool,
     }
 }
 
@@ -558,6 +564,27 @@ table! {
   }
 }
 
+table! {
+  email_verification (id) {
+    id -> Int4,
+    local_user_id -> Int4,
+    email -> Text,
+    verification_token -> Varchar,
+    published -> Timestamp,
+  }
+}
+
+table! {
+    registration_application (id) {
+        id -> Int4,
+        local_user_id -> Int4,
+        answer -> Text,
+        admin_id -> Nullable<Int4>,
+        deny_reason -> Nullable<Text>,
+        published -> Timestamp,
+    }
+}
+
 joinable!(comment_alias_1 -> person_alias_1 (creator_id));
 joinable!(comment -> comment_alias_1 (parent_id));
 joinable!(person_mention -> person_alias_1 (recipient_id));
@@ -619,6 +646,9 @@ joinable!(post_saved -> person (person_id));
 joinable!(post_saved -> post (post_id));
 joinable!(site -> person (creator_id));
 joinable!(site_aggregates -> site (site_id));
+joinable!(email_verification -> local_user (local_user_id));
+joinable!(registration_application -> local_user (local_user_id));
+joinable!(registration_application -> person (admin_id));
 
 allow_tables_to_appear_in_same_query!(
   activity,
@@ -662,4 +692,6 @@ allow_tables_to_appear_in_same_query!(
   comment_alias_1,
   person_alias_1,
   person_alias_2,
+  email_verification,
+  registration_application
 );
diff --git a/crates/db_schema/src/source/email_verification.rs b/crates/db_schema/src/source/email_verification.rs
new file mode 100644
index 00000000..e36f2901
--- /dev/null
+++ b/crates/db_schema/src/source/email_verification.rs
@@ -0,0 +1,19 @@
+use crate::{newtypes::LocalUserId, schema::email_verification};
+
+#[derive(Queryable, Identifiable, Clone)]
+#[table_name = "email_verification"]
+pub struct EmailVerification {
+  pub id: i32,
+  pub local_user_id: LocalUserId,
+  pub email: String,
+  pub verification_code: String,
+  pub published: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset)]
+#[table_name = "email_verification"]
+pub struct EmailVerificationForm {
+  pub local_user_id: LocalUserId,
+  pub email: String,
+  pub verification_token: String,
+}
diff --git a/crates/db_schema/src/source/local_user.rs b/crates/db_schema/src/source/local_user.rs
index 34dd26f7..88defa6d 100644
--- a/crates/db_schema/src/source/local_user.rs
+++ b/crates/db_schema/src/source/local_user.rs
@@ -23,14 +23,16 @@ pub struct LocalUser {
   pub show_scores: bool,
   pub show_read_posts: bool,
   pub show_new_post_notifs: bool,
+  pub email_verified: bool,
+  pub accepted_application: bool,
 }
 
 // TODO redo these, check table defaults
 #[derive(Insertable, AsChangeset, Clone, Default)]
 #[table_name = "local_user"]
 pub struct LocalUserForm {
-  pub person_id: PersonId,
-  pub password_encrypted: String,
+  pub person_id: Option<PersonId>,
+  pub password_encrypted: Option<String>,
   pub email: Option<Option<String>>,
   pub show_nsfw: Option<bool>,
   pub theme: Option<String>,
@@ -43,6 +45,8 @@ pub struct LocalUserForm {
   pub show_scores: Option<bool>,
   pub show_read_posts: Option<bool>,
   pub show_new_post_notifs: Option<bool>,
+  pub email_verified: Option<bool>,
+  pub accepted_application: Option<bool>,
 }
 
 /// A local user view that removes password encrypted
@@ -64,4 +68,6 @@ pub struct LocalUserSettings {
   pub show_scores: bool,
   pub show_read_posts: bool,
   pub show_new_post_notifs: bool,
+  pub email_verified: bool,
+  pub accepted_application: bool,
 }
diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs
index a1e45efa..c96e3a62 100644
--- a/crates/db_schema/src/source/mod.rs
+++ b/crates/db_schema/src/source/mod.rs
@@ -3,6 +3,7 @@ pub mod comment;
 pub mod comment_report;
 pub mod community;
 pub mod community_block;
+pub mod email_verification;
 pub mod local_user;
 pub mod moderator;
 pub mod password_reset_request;
@@ -12,5 +13,6 @@ pub mod person_mention;
 pub mod post;
 pub mod post_report;
 pub mod private_message;
+pub mod registration_application;
 pub mod secret;
 pub mod site;
diff --git a/crates/db_schema/src/source/registration_application.rs b/crates/db_schema/src/source/registration_application.rs
new file mode 100644
index 00000000..01f702d8
--- /dev/null
+++ b/crates/db_schema/src/source/registration_application.rs
@@ -0,0 +1,25 @@
+use crate::{
+  newtypes::{LocalUserId, PersonId},
+  schema::registration_application,
+};
+use serde::{Deserialize, Serialize};
+
+#[derive(Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name = "registration_application"]
+pub struct RegistrationApplication {
+  pub id: i32,
+  pub local_user_id: LocalUserId,
+  pub answer: String,
+  pub admin_id: Option<PersonId>,
+  pub deny_reason: Option<String>,
+  pub published: chrono::NaiveDateTime,
+}
+
+#[derive(Insertable, AsChangeset, Default)]
+#[table_name = "registration_application"]
+pub struct RegistrationApplicationForm {
+  pub local_user_id: Option<LocalUserId>,
+  pub answer: Option<String>,
+  pub admin_id: Option<PersonId>,
+  pub deny_reason: Option<Option<String>>,
+}
diff --git a/crates/db_schema/src/source/site.rs b/crates/db_schema/src/source/site.rs
index dd273f9d..f99ffd88 100644
--- a/crates/db_schema/src/source/site.rs
+++ b/crates/db_schema/src/source/site.rs
@@ -20,9 +20,13 @@ pub struct Site {
   pub banner: Option<DbUrl>,
   pub description: Option<String>,
   pub community_creation_admin_only: bool,
+  pub require_email_verification: bool,
+  pub require_application: bool,
+  pub application_question: Option<String>,
+  pub private_instance: bool,
 }
 
-#[derive(Insertable, AsChangeset)]
+#[derive(Insertable, AsChangeset, Default)]
 #[table_name = "site"]
 pub struct SiteForm {
   pub name: String,
@@ -37,4 +41,8 @@ pub struct SiteForm {
   pub banner: Option<Option<DbUrl>>,
   pub description: Option<Option<String>>,
   pub community_creation_admin_only: Option<bool>,
+  pub require_email_verification: Option<bool>,
+  pub require_application: Option<bool>,
+  pub application_question: Option<Option<String>>,
+  pub private_instance: Option<bool>,
 }
diff --git a/crates/db_views/Cargo.toml b/crates/db_views/Cargo.toml
index 53302daf..ec2f7077 100644
--- a/crates/db_views/Cargo.toml
+++ b/crates/db_views/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_db_views"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -11,7 +11,7 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
 doctest = false
 
 [dependencies]
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
 diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
 serde = { version = "1.0.131", features = ["derive"] }
 tracing = "0.1.29"
diff --git a/crates/db_views/src/lib.rs b/crates/db_views/src/lib.rs
index 54435c1e..cb9fefcf 100644
--- a/crates/db_views/src/lib.rs
+++ b/crates/db_views/src/lib.rs
@@ -7,4 +7,5 @@ pub mod local_user_view;
 pub mod post_report_view;
 pub mod post_view;
 pub mod private_message_view;
+pub mod registration_application_view;
 pub mod site_view;
diff --git a/crates/db_views/src/registration_application_view.rs b/crates/db_views/src/registration_application_view.rs
new file mode 100644
index 00000000..1a5fc9bb
--- /dev/null
+++ b/crates/db_views/src/registration_application_view.rs
@@ -0,0 +1,396 @@
+use diesel::{dsl::count, result::Error, *};
+use lemmy_db_schema::{
+  limit_and_offset,
+  schema::{local_user, person, person_alias_1, registration_application},
+  source::{
+    local_user::{LocalUser, LocalUserSettings},
+    person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1},
+    registration_application::RegistrationApplication,
+  },
+  traits::{MaybeOptional, ToSafe, ToSafeSettings, ViewToVec},
+};
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
+pub struct RegistrationApplicationView {
+  pub registration_application: RegistrationApplication,
+  pub creator_local_user: LocalUserSettings,
+  pub creator: PersonSafe,
+  pub admin: Option<PersonSafeAlias1>,
+}
+
+type RegistrationApplicationViewTuple = (
+  RegistrationApplication,
+  LocalUserSettings,
+  PersonSafe,
+  Option<PersonSafeAlias1>,
+);
+
+impl RegistrationApplicationView {
+  pub fn read(conn: &PgConnection, registration_application_id: i32) -> Result<Self, Error> {
+    let (registration_application, creator_local_user, creator, admin) =
+      registration_application::table
+        .find(registration_application_id)
+        .inner_join(
+          local_user::table.on(registration_application::local_user_id.eq(local_user::id)),
+        )
+        .inner_join(person::table.on(local_user::person_id.eq(person::id)))
+        .left_join(
+          person_alias_1::table
+            .on(registration_application::admin_id.eq(person_alias_1::id.nullable())),
+        )
+        .order_by(registration_application::published.desc())
+        .select((
+          registration_application::all_columns,
+          LocalUser::safe_settings_columns_tuple(),
+          Person::safe_columns_tuple(),
+          PersonAlias1::safe_columns_tuple().nullable(),
+        ))
+        .first::<RegistrationApplicationViewTuple>(conn)?;
+
+    Ok(RegistrationApplicationView {
+      registration_application,
+      creator_local_user,
+      creator,
+      admin,
+    })
+  }
+
+  /// Returns the current unread registration_application count
+  pub fn get_unread_count(conn: &PgConnection, verified_email_only: bool) -> Result<i64, Error> {
+    let mut query = registration_application::table
+      .inner_join(local_user::table.on(registration_application::local_user_id.eq(local_user::id)))
+      .inner_join(person::table.on(local_user::person_id.eq(person::id)))
+      .left_join(
+        person_alias_1::table
+          .on(registration_application::admin_id.eq(person_alias_1::id.nullable())),
+      )
+      .filter(registration_application::admin_id.is_null())
+      .into_boxed();
+
+    if verified_email_only {
+      query = query.filter(local_user::email_verified.eq(true))
+    }
+
+    query
+      .select(count(registration_application::id))
+      .first::<i64>(conn)
+  }
+}
+
+pub struct RegistrationApplicationQueryBuilder<'a> {
+  conn: &'a PgConnection,
+  unread_only: Option<bool>,
+  verified_email_only: Option<bool>,
+  page: Option<i64>,
+  limit: Option<i64>,
+}
+
+impl<'a> RegistrationApplicationQueryBuilder<'a> {
+  pub fn create(conn: &'a PgConnection) -> Self {
+    RegistrationApplicationQueryBuilder {
+      conn,
+      unread_only: None,
+      verified_email_only: None,
+      page: None,
+      limit: None,
+    }
+  }
+
+  pub fn unread_only<T: MaybeOptional<bool>>(mut self, unread_only: T) -> Self {
+    self.unread_only = unread_only.get_optional();
+    self
+  }
+
+  pub fn verified_email_only<T: MaybeOptional<bool>>(mut self, verified_email_only: T) -> Self {
+    self.verified_email_only = verified_email_only.get_optional();
+    self
+  }
+
+  pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+    self.page = page.get_optional();
+    self
+  }
+
+  pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+    self.limit = limit.get_optional();
+    self
+  }
+
+  pub fn list(self) -> Result<Vec<RegistrationApplicationView>, Error> {
+    let mut query = registration_application::table
+      .inner_join(local_user::table.on(registration_application::local_user_id.eq(local_user::id)))
+      .inner_join(person::table.on(local_user::person_id.eq(person::id)))
+      .left_join(
+        person_alias_1::table
+          .on(registration_application::admin_id.eq(person_alias_1::id.nullable())),
+      )
+      .order_by(registration_application::published.desc())
+      .select((
+        registration_application::all_columns,
+        LocalUser::safe_settings_columns_tuple(),
+        Person::safe_columns_tuple(),
+        PersonAlias1::safe_columns_tuple().nullable(),
+      ))
+      .into_boxed();
+
+    if self.unread_only.unwrap_or(false) {
+      query = query.filter(registration_application::admin_id.is_null())
+    }
+
+    if self.verified_email_only.unwrap_or(false) {
+      query = query.filter(local_user::email_verified.eq(true))
+    }
+
+    let (limit, offset) = limit_and_offset(self.page, self.limit);
+
+    query = query
+      .limit(limit)
+      .offset(offset)
+      .order_by(registration_application::published.desc());
+
+    let res = query.load::<RegistrationApplicationViewTuple>(self.conn)?;
+
+    Ok(RegistrationApplicationView::from_tuple_to_vec(res))
+  }
+}
+
+impl ViewToVec for RegistrationApplicationView {
+  type DbTuple = RegistrationApplicationViewTuple;
+  fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
+    items
+      .iter()
+      .map(|a| Self {
+        registration_application: a.0.to_owned(),
+        creator_local_user: a.1.to_owned(),
+        creator: a.2.to_owned(),
+        admin: a.3.to_owned(),
+      })
+      .collect::<Vec<Self>>()
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use crate::registration_application_view::{
+    RegistrationApplicationQueryBuilder,
+    RegistrationApplicationView,
+  };
+  use lemmy_db_schema::{
+    establish_unpooled_connection,
+    source::{
+      local_user::{LocalUser, LocalUserForm, LocalUserSettings},
+      person::*,
+      registration_application::{RegistrationApplication, RegistrationApplicationForm},
+    },
+    traits::Crud,
+  };
+  use serial_test::serial;
+
+  #[test]
+  #[serial]
+  fn test_crud() {
+    let conn = establish_unpooled_connection();
+
+    let timmy_person_form = PersonForm {
+      name: "timmy_rav".into(),
+      admin: Some(true),
+      ..PersonForm::default()
+    };
+
+    let inserted_timmy_person = Person::create(&conn, &timmy_person_form).unwrap();
+
+    let timmy_local_user_form = LocalUserForm {
+      person_id: Some(inserted_timmy_person.id),
+      password_encrypted: Some("nada".to_string()),
+      ..LocalUserForm::default()
+    };
+
+    let _inserted_timmy_local_user = LocalUser::create(&conn, &timmy_local_user_form).unwrap();
+
+    let sara_person_form = PersonForm {
+      name: "sara_rav".into(),
+      ..PersonForm::default()
+    };
+
+    let inserted_sara_person = Person::create(&conn, &sara_person_form).unwrap();
+
+    let sara_local_user_form = LocalUserForm {
+      person_id: Some(inserted_sara_person.id),
+      password_encrypted: Some("nada".to_string()),
+      ..LocalUserForm::default()
+    };
+
+    let inserted_sara_local_user = LocalUser::create(&conn, &sara_local_user_form).unwrap();
+
+    // Sara creates an application
+    let sara_app_form = RegistrationApplicationForm {
+      local_user_id: Some(inserted_sara_local_user.id),
+      answer: Some("LET ME IIIIINN".to_string()),
+      ..RegistrationApplicationForm::default()
+    };
+
+    let sara_app = RegistrationApplication::create(&conn, &sara_app_form).unwrap();
+
+    let read_sara_app_view = RegistrationApplicationView::read(&conn, sara_app.id).unwrap();
+
+    let jess_person_form = PersonForm {
+      name: "jess_rav".into(),
+      ..PersonForm::default()
+    };
+
+    let inserted_jess_person = Person::create(&conn, &jess_person_form).unwrap();
+
+    let jess_local_user_form = LocalUserForm {
+      person_id: Some(inserted_jess_person.id),
+      password_encrypted: Some("nada".to_string()),
+      ..LocalUserForm::default()
+    };
+
+    let inserted_jess_local_user = LocalUser::create(&conn, &jess_local_user_form).unwrap();
+
+    // Sara creates an application
+    let jess_app_form = RegistrationApplicationForm {
+      local_user_id: Some(inserted_jess_local_user.id),
+      answer: Some("LET ME IIIIINN".to_string()),
+      ..RegistrationApplicationForm::default()
+    };
+
+    let jess_app = RegistrationApplication::create(&conn, &jess_app_form).unwrap();
+
+    let read_jess_app_view = RegistrationApplicationView::read(&conn, jess_app.id).unwrap();
+
+    let mut expected_sara_app_view = RegistrationApplicationView {
+      registration_application: sara_app.to_owned(),
+      creator_local_user: LocalUserSettings {
+        id: inserted_sara_local_user.id,
+        person_id: inserted_sara_local_user.person_id,
+        email: inserted_sara_local_user.email,
+        show_nsfw: inserted_sara_local_user.show_nsfw,
+        theme: inserted_sara_local_user.theme,
+        default_sort_type: inserted_sara_local_user.default_sort_type,
+        default_listing_type: inserted_sara_local_user.default_listing_type,
+        lang: inserted_sara_local_user.lang,
+        show_avatars: inserted_sara_local_user.show_avatars,
+        send_notifications_to_email: inserted_sara_local_user.send_notifications_to_email,
+        validator_time: inserted_sara_local_user.validator_time,
+        show_bot_accounts: inserted_sara_local_user.show_bot_accounts,
+        show_scores: inserted_sara_local_user.show_scores,
+        show_read_posts: inserted_sara_local_user.show_read_posts,
+        show_new_post_notifs: inserted_sara_local_user.show_new_post_notifs,
+        email_verified: inserted_sara_local_user.email_verified,
+        accepted_application: inserted_sara_local_user.accepted_application,
+      },
+      creator: PersonSafe {
+        id: inserted_sara_person.id,
+        name: inserted_sara_person.name.to_owned(),
+        display_name: None,
+        published: inserted_sara_person.published,
+        avatar: None,
+        actor_id: inserted_sara_person.actor_id.to_owned(),
+        local: true,
+        banned: false,
+        deleted: false,
+        admin: false,
+        bot_account: false,
+        bio: None,
+        banner: None,
+        updated: None,
+        inbox_url: inserted_sara_person.inbox_url.to_owned(),
+        shared_inbox_url: None,
+        matrix_user_id: None,
+      },
+      admin: None,
+    };
+
+    assert_eq!(read_sara_app_view, expected_sara_app_view);
+
+    // Do a batch read of the applications
+    let apps = RegistrationApplicationQueryBuilder::create(&conn)
+      .unread_only(true)
+      .list()
+      .unwrap();
+
+    assert_eq!(
+      apps,
+      [
+        read_jess_app_view.to_owned(),
+        expected_sara_app_view.to_owned()
+      ]
+    );
+
+    // Make sure the counts are correct
+    let unread_count = RegistrationApplicationView::get_unread_count(&conn, false).unwrap();
+    assert_eq!(unread_count, 2);
+
+    // Approve the application
+    let approve_form = RegistrationApplicationForm {
+      admin_id: Some(inserted_timmy_person.id),
+      deny_reason: None,
+      ..RegistrationApplicationForm::default()
+    };
+
+    RegistrationApplication::update(&conn, sara_app.id, &approve_form).unwrap();
+
+    // Update the local_user row
+    let approve_local_user_form = LocalUserForm {
+      accepted_application: Some(true),
+      ..LocalUserForm::default()
+    };
+
+    LocalUser::update(&conn, inserted_sara_local_user.id, &approve_local_user_form).unwrap();
+
+    let read_sara_app_view_after_approve =
+      RegistrationApplicationView::read(&conn, sara_app.id).unwrap();
+
+    // Make sure the columns changed
+    expected_sara_app_view
+      .creator_local_user
+      .accepted_application = true;
+    expected_sara_app_view.registration_application.admin_id = Some(inserted_timmy_person.id);
+
+    expected_sara_app_view.admin = Some(PersonSafeAlias1 {
+      id: inserted_timmy_person.id,
+      name: inserted_timmy_person.name.to_owned(),
+      display_name: None,
+      published: inserted_timmy_person.published,
+      avatar: None,
+      actor_id: inserted_timmy_person.actor_id.to_owned(),
+      local: true,
+      banned: false,
+      deleted: false,
+      admin: true,
+      bot_account: false,
+      bio: None,
+      banner: None,
+      updated: None,
+      inbox_url: inserted_timmy_person.inbox_url.to_owned(),
+      shared_inbox_url: None,
+      matrix_user_id: None,
+    });
+    assert_eq!(read_sara_app_view_after_approve, expected_sara_app_view);
+
+    // Do a batch read of apps again
+    // It should show only jessicas which is unresolved
+    let apps_after_resolve = RegistrationApplicationQueryBuilder::create(&conn)
+      .unread_only(true)
+      .list()
+      .unwrap();
+    assert_eq!(apps_after_resolve, vec![read_jess_app_view]);
+
+    // Make sure the counts are correct
+    let unread_count_after_approve =
+      RegistrationApplicationView::get_unread_count(&conn, false).unwrap();
+    assert_eq!(unread_count_after_approve, 1);
+
+    // Make sure the not undenied_only has all the apps
+    let all_apps = RegistrationApplicationQueryBuilder::create(&conn)
+      .list()
+      .unwrap();
+    assert_eq!(all_apps.len(), 2);
+
+    Person::delete(&conn, inserted_timmy_person.id).unwrap();
+    Person::delete(&conn, inserted_sara_person.id).unwrap();
+    Person::delete(&conn, inserted_jess_person.id).unwrap();
+  }
+}
diff --git a/crates/db_views_actor/Cargo.toml b/crates/db_views_actor/Cargo.toml
index 488e1f62..f94324af 100644
--- a/crates/db_views_actor/Cargo.toml
+++ b/crates/db_views_actor/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_db_views_actor"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -11,6 +11,6 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
 doctest = false
 
 [dependencies]
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
 diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
 serde = { version = "1.0.131", features = ["derive"] }
diff --git a/crates/db_views_moderator/Cargo.toml b/crates/db_views_moderator/Cargo.toml
index adebabc5..1a234807 100644
--- a/crates/db_views_moderator/Cargo.toml
+++ b/crates/db_views_moderator/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_db_views_moderator"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -11,6 +11,6 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
 doctest = false
 
 [dependencies]
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
 diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"] }
 serde = { version = "1.0.131", features = ["derive"] }
diff --git a/crates/routes/Cargo.toml b/crates/routes/Cargo.toml
index 33597e62..a832c17a 100644
--- a/crates/routes/Cargo.toml
+++ b/crates/routes/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_routes"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -11,13 +11,13 @@ documentation = "https://join-lemmy.org/docs/en/index.html"
 doctest = false
 
 [dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_websocket = { version = "=0.14.4-rc.4", path = "../websocket" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_apub = { version = "=0.14.4-rc.4", path = "../apub" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_websocket = { version = "=0.15.0-rc.6", path = "../websocket" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_apub = { version = "=0.15.0-rc.6", path = "../apub" }
 diesel = "1.4.8"
 actix = "0.12.0"
 actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["rustls"] }
diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml
index b0da4cb7..20fcef5b 100644
--- a/crates/utils/Cargo.toml
+++ b/crates/utils/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_utils"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
diff --git a/crates/utils/src/email.rs b/crates/utils/src/email.rs
index eb5e0d1c..d9ac1710 100644
--- a/crates/utils/src/email.rs
+++ b/crates/utils/src/email.rs
@@ -1,4 +1,4 @@
-use crate::settings::structs::Settings;
+use crate::{settings::structs::Settings, LemmyError};
 use lettre::{
   message::{header, Mailbox, MultiPart, SinglePart},
   transport::smtp::{
@@ -20,12 +20,21 @@ pub fn send_email(
   to_username: &str,
   html: &str,
   settings: &Settings,
-) -> Result<(), String> {
-  let email_config = settings.email.to_owned().ok_or("no_email_setup")?;
+) -> Result<(), LemmyError> {
+  let email_config = settings
+    .email
+    .to_owned()
+    .ok_or_else(|| LemmyError::from_message("no_email_setup"))?;
   let domain = settings.hostname.to_owned();
 
   let (smtp_server, smtp_port) = {
     let email_and_port = email_config.smtp_server.split(':').collect::<Vec<&str>>();
+    if email_and_port.len() == 1 {
+      return Err(LemmyError::from_message(
+        "email.smtp_server needs a port, IE smtp.xxx.com:465",
+      ));
+    }
+
     (
       email_and_port[0],
       email_and_port[1]
@@ -87,6 +96,6 @@ pub fn send_email(
 
   match result {
     Ok(_) => Ok(()),
-    Err(e) => Err(e.to_string()),
+    Err(e) => Err(LemmyError::from(e).with_message("email_send_failed")),
   }
 }
diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs
index ae7bd544..2de87c5b 100644
--- a/crates/utils/src/settings/structs.rs
+++ b/crates/utils/src/settings/structs.rs
@@ -190,4 +190,12 @@ pub struct SetupConfig {
   pub enable_nsfw: Option<bool>,
   #[default(None)]
   pub community_creation_admin_only: Option<bool>,
+  #[default(None)]
+  pub require_email_verification: Option<bool>,
+  #[default(None)]
+  pub require_application: Option<bool>,
+  #[default(None)]
+  pub application_question: Option<String>,
+  #[default(None)]
+  pub private_instance: Option<bool>,
 }
diff --git a/crates/websocket/Cargo.toml b/crates/websocket/Cargo.toml
index 2099fc95..f1167178 100644
--- a/crates/websocket/Cargo.toml
+++ b/crates/websocket/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "lemmy_websocket"
-version = "0.14.4-rc.4"
+version = "0.15.0-rc.6"
 edition = "2018"
 description = "A link aggregator for the fediverse"
 license = "AGPL-3.0"
@@ -13,11 +13,11 @@ path = "src/lib.rs"
 doctest = false
 
 [dependencies]
-lemmy_utils = { version = "=0.14.4-rc.4", path = "../utils" }
-lemmy_api_common = { version = "=0.14.4-rc.4", path = "../api_common" }
-lemmy_db_schema = { version = "=0.14.4-rc.4", path = "../db_schema" }
-lemmy_db_views = { version = "=0.14.4-rc.4", path = "../db_views" }
-lemmy_db_views_actor = { version = "=0.14.4-rc.4", path = "../db_views_actor" }
+lemmy_utils = { version = "=0.15.0-rc.6", path = "../utils" }
+lemmy_api_common = { version = "=0.15.0-rc.6", path = "../api_common" }
+lemmy_db_schema = { version = "=0.15.0-rc.6", path = "../db_schema" }
+lemmy_db_views = { version = "=0.15.0-rc.6", path = "../db_views" }
+lemmy_db_views_actor = { version = "=0.15.0-rc.6", path = "../db_views_actor" }
 reqwest = { version = "0.11.7", features = ["json"] }
 reqwest-middleware = "0.1.3"
 tracing = "0.1.29"
diff --git a/crates/websocket/src/lib.rs b/crates/websocket/src/lib.rs
index 554717d4..5a132a18 100644
--- a/crates/websocket/src/lib.rs
+++ b/crates/websocket/src/lib.rs
@@ -117,6 +117,7 @@ pub enum UserOperation {
   ListPostReports,
   GetReportCount,
   GetUnreadCount,
+  VerifyEmail,
   FollowCommunity,
   GetReplies,
   GetPersonMentions,
@@ -125,6 +126,9 @@ pub enum UserOperation {
   BanFromCommunity,
   AddModToCommunity,
   AddAdmin,
+  GetUnreadRegistrationApplicationCount,
+  ListRegistrationApplications,
+  ApproveRegistrationApplication,
   BanPerson,
   Search,
   ResolveObject,
diff --git a/crates/websocket/src/send.rs b/crates/websocket/src/send.rs
index ac0752c6..e7f265b5 100644
--- a/crates/websocket/src/send.rs
+++ b/crates/websocket/src/send.rs
@@ -10,6 +10,7 @@ use lemmy_api_common::{
   community::CommunityResponse,
   person::PrivateMessageResponse,
   post::PostResponse,
+  send_email_to_user,
 };
 use lemmy_db_schema::{
   newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId},
@@ -28,14 +29,7 @@ use lemmy_db_views::{
   private_message_view::PrivateMessageView,
 };
 use lemmy_db_views_actor::community_view::CommunityView;
-use lemmy_utils::{
-  email::send_email,
-  settings::structs::Settings,
-  utils::MentionData,
-  ConnectionId,
-  LemmyError,
-};
-use tracing::error;
+use lemmy_utils::{utils::MentionData, ConnectionId, LemmyError};
 
 pub async fn send_post_ws_message<OP: ToString + Send + OperationType + 'static>(
   post_id: PostId,
@@ -296,39 +290,3 @@ pub async fn send_local_notifs(
   };
   Ok(recipient_ids)
 }
-
-pub fn send_email_to_user(
-  local_user_view: &LocalUserView,
-  subject_text: &str,
-  body_text: &str,
-  comment_content: &str,
-  settings: &Settings,
-) {
-  if local_user_view.person.banned || !local_user_view.local_user.send_notifications_to_email {
-    return;
-  }
-
-  if let Some(user_email) = &local_user_view.local_user.email {
-    let subject = &format!(
-      "{} - {} {}",
-      subject_text, settings.hostname, local_user_view.person.name,
-    );
-    let html = &format!(
-      "<h1>{}</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
-      body_text,
-      local_user_view.person.name,
-      comment_content,
-      settings.get_protocol_and_hostname()
-    );
-    match send_email(
-      subject,
-      user_email,
-      &local_user_view.person.name,
-      html,
-      settings,
-    ) {
-      Ok(_o) => _o,
-      Err(e) => error!("{}", e),
-    };
-  }
-}
diff --git a/migrations/2021-11-23-132840_email_verification/down.sql b/migrations/2021-11-23-132840_email_verification/down.sql
new file mode 100644
index 00000000..cd1eb7c2
--- /dev/null
+++ b/migrations/2021-11-23-132840_email_verification/down.sql
@@ -0,0 +1,8 @@
+-- revert defaults from db for local user init
+alter table local_user alter column theme set default 'darkly';
+alter table local_user alter column default_listing_type set default 1;
+
+-- remove tables and columns for optional email verification
+alter table site drop column require_email_verification;
+alter table local_user drop column email_verified;
+drop table email_verification;
diff --git a/migrations/2021-11-23-132840_email_verification/up.sql b/migrations/2021-11-23-132840_email_verification/up.sql
new file mode 100644
index 00000000..29a20e00
--- /dev/null
+++ b/migrations/2021-11-23-132840_email_verification/up.sql
@@ -0,0 +1,14 @@
+-- use defaults from db for local user init
+alter table local_user alter column theme set default 'browser';
+alter table local_user alter column default_listing_type set default 2;
+
+-- add tables and columns for optional email verification
+alter table site add column require_email_verification boolean not null default false;
+alter table local_user add column email_verified boolean not null default false;
+
+create table email_verification (
+    id serial primary key,
+    local_user_id int references local_user(id) on update cascade on delete cascade not null,
+    email text not null,
+    verification_token text not null
+);
diff --git a/migrations/2021-11-23-153753_add_invite_only_columns/down.sql b/migrations/2021-11-23-153753_add_invite_only_columns/down.sql
new file mode 100644
index 00000000..52a1a280
--- /dev/null
+++ b/migrations/2021-11-23-153753_add_invite_only_columns/down.sql
@@ -0,0 +1,9 @@
+-- Add columns to site table
+alter table site drop column require_application;
+alter table site drop column application_question;
+alter table site drop column private_instance;
+
+-- Add pending to local_user
+alter table local_user drop column accepted_application;
+
+drop table registration_application;
diff --git a/migrations/2021-11-23-153753_add_invite_only_columns/up.sql b/migrations/2021-11-23-153753_add_invite_only_columns/up.sql
new file mode 100644
index 00000000..b3f8a18d
--- /dev/null
+++ b/migrations/2021-11-23-153753_add_invite_only_columns/up.sql
@@ -0,0 +1,19 @@
+-- Add columns to site table
+alter table site add column require_application boolean not null default false;
+alter table site add column application_question text;
+alter table site add column private_instance boolean not null default false;
+
+-- Add pending to local_user
+alter table local_user add column accepted_application boolean not null default false;
+
+create table registration_application (
+  id serial primary key,
+  local_user_id int references local_user on update cascade on delete cascade not null,
+  answer text not null,
+  admin_id int references person on update cascade on delete cascade,
+  deny_reason text,
+  published timestamp not null default now(),
+  unique(local_user_id)
+);
+
+create index idx_registration_application_published on registration_application (published desc);
diff --git a/migrations/2021-12-09-225529_add_published_to_email_verification/down.sql b/migrations/2021-12-09-225529_add_published_to_email_verification/down.sql
new file mode 100644
index 00000000..21405db1
--- /dev/null
+++ b/migrations/2021-12-09-225529_add_published_to_email_verification/down.sql
@@ -0,0 +1 @@
+alter table email_verification drop column published;
diff --git a/migrations/2021-12-09-225529_add_published_to_email_verification/up.sql b/migrations/2021-12-09-225529_add_published_to_email_verification/up.sql
new file mode 100644
index 00000000..79dd32bf
--- /dev/null
+++ b/migrations/2021-12-09-225529_add_published_to_email_verification/up.sql
@@ -0,0 +1 @@
+alter table email_verification add column published timestamp not null default now();
diff --git a/src/api_routes.rs b/src/api_routes.rs
index 3fbb7f05..88466485 100644
--- a/src/api_routes.rs
+++ b/src/api_routes.rs
@@ -210,13 +210,26 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
             web::put().to(route_post::<ChangePassword>),
           )
           .route("/report_count", web::get().to(route_get::<GetReportCount>))
-          .route("/unread_count", web::get().to(route_get::<GetUnreadCount>)),
+          .route("/unread_count", web::get().to(route_get::<GetUnreadCount>))
+          .route("/verify_email", web::post().to(route_post::<VerifyEmail>)),
       )
       // Admin Actions
       .service(
-        web::resource("/admin/add")
+        web::scope("/admin")
           .wrap(rate_limit.message())
-          .route(web::post().to(route_post::<AddAdmin>)),
+          .route("/add", web::post().to(route_post::<AddAdmin>))
+          .route(
+            "/registration_application/count",
+            web::get().to(route_get::<GetUnreadRegistrationApplicationCount>),
+          )
+          .route(
+            "/registration_application/list",
+            web::get().to(route_get::<ListRegistrationApplications>),
+          )
+          .route(
+            "/registration_application/approve",
+            web::put().to(route_post::<ApproveRegistrationApplication>),
+          ),
       ),
   );
 }
diff --git a/src/main.rs b/src/main.rs
index cf29e779..252d37a3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,7 +9,7 @@ use diesel::{
 };
 use doku::json::{AutoComments, Formatting};
 use lemmy_api::match_websocket_operation;
-use lemmy_api_common::blocking;
+use lemmy_api_common::{blocking, check_private_instance_and_federation_enabled};
 use lemmy_api_crud::match_websocket_operation_crud;
 use lemmy_apub_lib::activity_queue::create_activity_queue;
 use lemmy_db_schema::{get_database_url_from_env, source::secret::Secret};
@@ -103,6 +103,8 @@ async fn main() -> Result<(), LemmyError> {
 
   let activity_queue = queue_manager.queue_handle().clone();
 
+  check_private_instance_and_federation_enabled(&pool, &settings).await?;
+
   let chat_server = ChatServer::startup(
     pool.clone(),
     rate_limiter.clone(),
-- 
2.44.1