From f5f9861a78c1a4b6322e27739e7886d8d0f15759 Mon Sep 17 00:00:00 2001
From: Michael Powers <swedishborgie@gmail.com>
Date: Thu, 12 Sep 2019 16:12:22 -0400
Subject: [PATCH] Adds support for PostgreSQL which resolves #87 and is
 mentioned in #246. This includes migrations as well as Dockerfile's for
 amd64.

The biggest change is that replace_into isn't supported by Diesel for the
PostgreSQL backend, instead requiring the use of on_conflict. This
unfortunately requires a branch for save() on all of the models currently
using replace_into.
---
 Cargo.lock                                    |  12 ++
 Cargo.toml                                    |   4 +
 build.rs                                      |   8 +-
 docker/amd64/postgresql/Dockerfile            | 103 +++++++++++
 docker/amd64/postgresql/Dockerfile.alpine     |  85 +++++++++
 .../2019-09-12-100000_create_tables/down.sql  |  13 ++
 .../2019-09-12-100000_create_tables/up.sql    | 121 ++++++++++++
 src/db/mod.rs                                 |   5 +
 src/db/models/attachment.rs                   |  16 +-
 src/db/models/cipher.rs                       |  17 +-
 src/db/models/collection.rs                   |  49 ++++-
 src/db/models/device.rs                       |  14 +-
 src/db/models/folder.rs                       |  28 ++-
 src/db/models/organization.rs                 |  36 +++-
 src/db/models/two_factor.rs                   |  14 +-
 src/db/models/user.rs                         |  35 +++-
 src/db/schemas/postgresql/schema.rs           | 172 ++++++++++++++++++
 src/main.rs                                   |   4 +
 18 files changed, 724 insertions(+), 12 deletions(-)
 create mode 100644 docker/amd64/postgresql/Dockerfile
 create mode 100644 docker/amd64/postgresql/Dockerfile.alpine
 create mode 100644 migrations/postgresql/2019-09-12-100000_create_tables/down.sql
 create mode 100644 migrations/postgresql/2019-09-12-100000_create_tables/up.sql
 create mode 100644 src/db/schemas/postgresql/schema.rs

diff --git a/Cargo.lock b/Cargo.lock
index d3e686e7..148d8641 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -116,6 +116,7 @@ dependencies = [
  "num-derive 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "oath 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "openssl 0.10.24 (registry+https://github.com/rust-lang/crates.io-index)",
  "percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "quoted_printable 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -419,11 +420,13 @@ name = "diesel"
 version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
+ "bitflags 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "byteorder 1.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "chrono 0.4.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "diesel_derives 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "libsqlite3-sys 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "mysqlclient-sys 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "r2d2 0.8.5 (registry+https://github.com/rust-lang/crates.io-index)",
  "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -1649,6 +1652,14 @@ name = "ppv-lite86"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "pq-sys"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "vcpkg 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "precomputed-hash"
 version = "0.1.1"
@@ -3095,6 +3106,7 @@ dependencies = [
 "checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0"
 "checksum pkg-config 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "a7c1d2cfa5a714db3b5f24f0915e74fcdf91d09d496ba61329705dda7774d2af"
 "checksum ppv-lite86 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e3cbf9f658cdb5000fcf6f362b8ea2ba154b9f146a61c7a20d647034c6b6561b"
+"checksum pq-sys 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
 "checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
 "checksum proc-macro-hack 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "463bf29e7f11344e58c9e01f171470ab15c925c6822ad75028cc1c0e1d1eb63b"
 "checksum proc-macro-hack-impl 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "38c47dcb1594802de8c02f3b899e2018c78291168a22c281be21ea0fb4796842"
diff --git a/Cargo.toml b/Cargo.toml
index 056ccbfb..fc99257a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,6 +14,7 @@ build = "build.rs"
 # Empty to keep compatibility, prefer to set USE_SYSLOG=true
 enable_syslog = []
 mysql = ["diesel/mysql", "diesel_migrations/mysql"]
+postgresql = ["diesel/postgres", "diesel_migrations/postgres", "openssl"]
 sqlite = ["diesel/sqlite", "diesel_migrations/sqlite", "libsqlite3-sys"]
 
 [target."cfg(not(windows))".dependencies]
@@ -105,6 +106,9 @@ handlebars = "2.0.2"
 soup = "0.4.1"
 regex = "1.3.1"
 
+# Required for SSL support for PostgreSQL
+openssl = { version = "0.10.24", optional = true }
+
 # URL encoding library
 percent-encoding = "2.1.0"
 
diff --git a/build.rs b/build.rs
index e950653d..ac92dbd9 100644
--- a/build.rs
+++ b/build.rs
@@ -2,9 +2,13 @@ use std::process::Command;
 
 fn main() {
     #[cfg(all(feature = "sqlite", feature = "mysql"))]
-    compile_error!("Can't enable both backends");
+    compile_error!("Can't enable both sqlite and mysql at the same time");
+    #[cfg(all(feature = "sqlite", feature = "postgresql"))]
+    compile_error!("Can't enable both sqlite and postgresql at the same time");
+    #[cfg(all(feature = "mysql", feature = "postgresql"))]
+    compile_error!("Can't enable both mysql and postgresql at the same time");
 
-    #[cfg(not(any(feature = "sqlite", feature = "mysql")))]
+    #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))]
     compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite");
 
     read_git_info().ok();
diff --git a/docker/amd64/postgresql/Dockerfile b/docker/amd64/postgresql/Dockerfile
new file mode 100644
index 00000000..fd33b0df
--- /dev/null
+++ b/docker/amd64/postgresql/Dockerfile
@@ -0,0 +1,103 @@
+# Using multistage build: 
+# 	https://docs.docker.com/develop/develop-images/multistage-build/
+# 	https://whitfin.io/speeding-up-rust-docker-builds/
+####################### VAULT BUILD IMAGE  #######################
+FROM alpine:3.10 as vault
+
+ENV VAULT_VERSION "v2.12.0"
+
+ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
+
+RUN apk add --no-cache --upgrade \
+    curl \
+    tar
+
+RUN mkdir /web-vault
+WORKDIR /web-vault
+
+SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
+
+RUN curl -L $URL | tar xz
+RUN ls
+
+########################## BUILD IMAGE  ##########################
+# We need to use the Rust build image, because
+# we need the Rust compiler and Cargo tooling
+FROM rust:1.36 as build
+
+# set mysql backend
+ARG DB=postgresql
+
+# Using bundled SQLite, no need to install it
+# RUN apt-get update && apt-get install -y\
+#    --no-install-recommends \
+#    sqlite3\
+# && rm -rf /var/lib/apt/lists/*
+
+# Install MySQL package
+RUN apt-get update && apt-get install -y \
+    --no-install-recommends \
+    libpq-dev \
+    && rm -rf /var/lib/apt/lists/*
+
+# Creates a dummy project used to grab dependencies
+RUN USER=root cargo new --bin app
+WORKDIR /app
+
+# Copies over *only* your manifests and build files
+COPY ./Cargo.* ./
+COPY ./rust-toolchain ./rust-toolchain
+COPY ./build.rs ./build.rs
+
+# Builds your dependencies and removes the
+# dummy project, except the target folder
+# This folder contains the compiled dependencies
+RUN cargo build --features ${DB} --release
+RUN find . -not -path "./target*" -delete
+
+# Copies the complete project
+# To avoid copying unneeded files, use .dockerignore
+COPY . .
+
+# Make sure that we actually build the project
+RUN touch src/main.rs
+
+# Builds again, this time it'll just be
+# your actual source files being built
+RUN cargo build --features ${DB} --release
+
+######################## RUNTIME IMAGE  ########################
+# Create a new stage with a minimal image
+# because we already have a binary built
+FROM debian:stretch-slim
+
+ENV ROCKET_ENV "staging"
+ENV ROCKET_PORT=80
+ENV ROCKET_WORKERS=10
+
+# Install needed libraries
+RUN apt-get update && apt-get install -y \
+    --no-install-recommends \
+    openssl \
+    ca-certificates \
+    curl \
+    libpq5 \
+    && rm -rf /var/lib/apt/lists/*
+
+RUN mkdir /data
+VOLUME /data
+EXPOSE 80
+EXPOSE 3012
+
+# Copies the files from the context (Rocket.toml file and web-vault)
+# and the binary from the "build" stage to the current stage
+COPY Rocket.toml .
+COPY --from=vault /web-vault ./web-vault
+COPY --from=build app/target/release/bitwarden_rs .
+
+COPY docker/healthcheck.sh ./healthcheck.sh
+
+HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
+
+# Configures the startup!
+CMD ["./bitwarden_rs"]
\ No newline at end of file
diff --git a/docker/amd64/postgresql/Dockerfile.alpine b/docker/amd64/postgresql/Dockerfile.alpine
new file mode 100644
index 00000000..0a13b8ea
--- /dev/null
+++ b/docker/amd64/postgresql/Dockerfile.alpine
@@ -0,0 +1,85 @@
+# Using multistage build: 
+# 	https://docs.docker.com/develop/develop-images/multistage-build/
+# 	https://whitfin.io/speeding-up-rust-docker-builds/
+####################### VAULT BUILD IMAGE  #######################
+FROM alpine:3.10 as vault
+
+ENV VAULT_VERSION "v2.12.0"
+
+ENV URL "https://github.com/dani-garcia/bw_web_builds/releases/download/$VAULT_VERSION/bw_web_$VAULT_VERSION.tar.gz"
+
+RUN apk add --no-cache --upgrade \
+    curl \
+    tar
+
+RUN mkdir /web-vault
+WORKDIR /web-vault
+
+SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
+
+RUN curl -L $URL | tar xz
+RUN ls
+
+########################## BUILD IMAGE  ##########################
+# Musl build image for statically compiled binary
+FROM clux/muslrust:nightly-2019-07-08 as build
+
+# set mysql backend
+ARG DB=postgresql
+
+ENV USER "root"
+
+# Install needed libraries
+RUN apt-get update && apt-get install -y \
+    --no-install-recommends \
+    libpq-dev \
+    && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Copies the complete project
+# To avoid copying unneeded files, use .dockerignore
+COPY . .
+
+RUN rustup target add x86_64-unknown-linux-musl
+
+# Make sure that we actually build the project
+RUN touch src/main.rs
+
+# Build
+RUN cargo build --features ${DB} --release
+
+######################## RUNTIME IMAGE  ########################
+# Create a new stage with a minimal image
+# because we already have a binary built
+FROM alpine:3.10
+
+ENV ROCKET_ENV "staging"
+ENV ROCKET_PORT=80
+ENV ROCKET_WORKERS=10
+ENV SSL_CERT_DIR=/etc/ssl/certs
+
+# Install needed libraries
+RUN apk add --no-cache \
+        openssl \
+        postgresql-libs \
+        curl \
+        ca-certificates
+
+RUN mkdir /data
+VOLUME /data
+EXPOSE 80
+EXPOSE 3012
+
+# Copies the files from the context (Rocket.toml file and web-vault)
+# and the binary from the "build" stage to the current stage
+COPY Rocket.toml .
+COPY --from=vault /web-vault ./web-vault
+COPY --from=build /app/target/x86_64-unknown-linux-musl/release/bitwarden_rs .
+
+COPY docker/healthcheck.sh ./healthcheck.sh
+
+HEALTHCHECK --interval=30s --timeout=3s CMD sh healthcheck.sh || exit 1
+
+# Configures the startup!
+CMD ["./bitwarden_rs"]
\ No newline at end of file
diff --git a/migrations/postgresql/2019-09-12-100000_create_tables/down.sql b/migrations/postgresql/2019-09-12-100000_create_tables/down.sql
new file mode 100644
index 00000000..e4561c37
--- /dev/null
+++ b/migrations/postgresql/2019-09-12-100000_create_tables/down.sql
@@ -0,0 +1,13 @@
+DROP TABLE devices;
+DROP TABLE attachments;
+DROP TABLE users_collections;
+DROP TABLE users_organizations;
+DROP TABLE folders_ciphers;
+DROP TABLE ciphers_collections;
+DROP TABLE twofactor;
+DROP TABLE invitations;
+DROP TABLE collections;
+DROP TABLE folders;
+DROP TABLE ciphers;
+DROP TABLE users;
+DROP TABLE organizations;
diff --git a/migrations/postgresql/2019-09-12-100000_create_tables/up.sql b/migrations/postgresql/2019-09-12-100000_create_tables/up.sql
new file mode 100644
index 00000000..c747e9aa
--- /dev/null
+++ b/migrations/postgresql/2019-09-12-100000_create_tables/up.sql
@@ -0,0 +1,121 @@
+CREATE TABLE users (
+  uuid                CHAR(36) NOT NULL PRIMARY KEY,
+  created_at          TIMESTAMP NOT NULL,
+  updated_at          TIMESTAMP NOT NULL,
+  email               VARCHAR(255) NOT NULL UNIQUE,
+  name                TEXT     NOT NULL,
+  password_hash       BYTEA     NOT NULL,
+  salt                BYTEA     NOT NULL,
+  password_iterations INTEGER  NOT NULL,
+  password_hint       TEXT,
+  akey                TEXT     NOT NULL,
+  private_key         TEXT,
+  public_key          TEXT,
+  totp_secret         TEXT,
+  totp_recover        TEXT,
+  security_stamp      TEXT     NOT NULL,
+  equivalent_domains  TEXT     NOT NULL,
+  excluded_globals    TEXT     NOT NULL,
+  client_kdf_type     INTEGER NOT NULL DEFAULT 0,
+  client_kdf_iter INTEGER NOT NULL DEFAULT 100000
+);
+
+CREATE TABLE devices (
+  uuid          CHAR(36) NOT NULL PRIMARY KEY,
+  created_at    TIMESTAMP NOT NULL,
+  updated_at    TIMESTAMP NOT NULL,
+  user_uuid     CHAR(36) NOT NULL REFERENCES users (uuid),
+  name          TEXT     NOT NULL,
+  atype         INTEGER  NOT NULL,
+  push_token    TEXT,
+  refresh_token TEXT     NOT NULL,
+  twofactor_remember TEXT
+);
+
+CREATE TABLE organizations (
+  uuid          VARCHAR(40) NOT NULL PRIMARY KEY,
+  name          TEXT NOT NULL,
+  billing_email TEXT NOT NULL
+);
+
+CREATE TABLE ciphers (
+  uuid              CHAR(36) NOT NULL PRIMARY KEY,
+  created_at        TIMESTAMP NOT NULL,
+  updated_at        TIMESTAMP NOT NULL,
+  user_uuid         CHAR(36) REFERENCES users (uuid),
+  organization_uuid CHAR(36) REFERENCES organizations (uuid),
+  atype             INTEGER  NOT NULL,
+  name              TEXT     NOT NULL,
+  notes             TEXT,
+  fields            TEXT,
+  data              TEXT     NOT NULL,
+  favorite          BOOLEAN  NOT NULL,
+  password_history  TEXT
+);
+
+CREATE TABLE attachments (
+  id          CHAR(36) NOT NULL PRIMARY KEY,
+  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
+  file_name   TEXT    NOT NULL,
+  file_size   INTEGER NOT NULL,
+  akey        TEXT
+);
+
+CREATE TABLE folders (
+  uuid       CHAR(36) NOT NULL PRIMARY KEY,
+  created_at TIMESTAMP NOT NULL,
+  updated_at TIMESTAMP NOT NULL,
+  user_uuid  CHAR(36) NOT NULL REFERENCES users (uuid),
+  name       TEXT     NOT NULL
+);
+
+CREATE TABLE collections (
+  uuid     VARCHAR(40) NOT NULL PRIMARY KEY,
+  org_uuid VARCHAR(40) NOT NULL REFERENCES organizations (uuid),
+  name     TEXT NOT NULL
+);
+
+CREATE TABLE users_collections (
+  user_uuid       CHAR(36) NOT NULL REFERENCES users (uuid),
+  collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),
+  read_only       BOOLEAN NOT NULL DEFAULT false,
+  PRIMARY KEY (user_uuid, collection_uuid)
+);
+
+CREATE TABLE users_organizations (
+  uuid       CHAR(36) NOT NULL PRIMARY KEY,
+  user_uuid  CHAR(36) NOT NULL REFERENCES users (uuid),
+  org_uuid   CHAR(36) NOT NULL REFERENCES organizations (uuid),
+
+  access_all BOOLEAN NOT NULL,
+  akey       TEXT    NOT NULL,
+  status     INTEGER NOT NULL,
+  atype      INTEGER NOT NULL,
+
+  UNIQUE (user_uuid, org_uuid)
+);
+
+CREATE TABLE folders_ciphers (
+  cipher_uuid CHAR(36) NOT NULL REFERENCES ciphers (uuid),
+  folder_uuid CHAR(36) NOT NULL REFERENCES folders (uuid),
+  PRIMARY KEY (cipher_uuid, folder_uuid)
+);
+
+CREATE TABLE ciphers_collections (
+  cipher_uuid       CHAR(36) NOT NULL REFERENCES ciphers (uuid),
+  collection_uuid CHAR(36) NOT NULL REFERENCES collections (uuid),
+  PRIMARY KEY (cipher_uuid, collection_uuid)
+);
+
+CREATE TABLE twofactor (
+  uuid      CHAR(36) NOT NULL PRIMARY KEY,
+  user_uuid CHAR(36) NOT NULL REFERENCES users (uuid),
+  atype     INTEGER  NOT NULL,
+  enabled   BOOLEAN  NOT NULL,
+  data      TEXT     NOT NULL,
+  UNIQUE (user_uuid, atype)
+);
+
+CREATE TABLE invitations (
+    email   VARCHAR(255) NOT NULL PRIMARY KEY
+);
\ No newline at end of file
diff --git a/src/db/mod.rs b/src/db/mod.rs
index 010f213d..a7a32eed 100644
--- a/src/db/mod.rs
+++ b/src/db/mod.rs
@@ -19,6 +19,8 @@ use crate::CONFIG;
 type Connection = diesel::sqlite::SqliteConnection;
 #[cfg(feature = "mysql")]
 type Connection = diesel::mysql::MysqlConnection;
+#[cfg(feature = "postgresql")]
+type Connection = diesel::pg::PgConnection;
 
 /// An alias to the type for a pool of Diesel connections.
 type Pool = r2d2::Pool<ConnectionManager<Connection>>;
@@ -33,6 +35,9 @@ pub mod schema;
 #[cfg(feature = "mysql")]
 #[path = "schemas/mysql/schema.rs"]
 pub mod schema;
+#[cfg(feature = "postgresql")]
+#[path = "schemas/postgresql/schema.rs"]
+pub mod schema;
 
 /// Initializes a database pool.
 pub fn init_pool() -> Pool {
diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs
index 030400e7..0f8e495f 100644
--- a/src/db/models/attachment.rs
+++ b/src/db/models/attachment.rs
@@ -3,7 +3,7 @@ use serde_json::Value;
 use super::Cipher;
 use crate::CONFIG;
 
-#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
+#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "attachments"]
 #[belongs_to(Cipher, foreign_key = "cipher_uuid")]
 #[primary_key(id)]
@@ -59,8 +59,20 @@ use crate::error::MapResult;
 
 /// Database methods
 impl Attachment {
+    #[cfg(feature = "postgresql")]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
-        diesel::replace_into(attachments::table)
+        return diesel::insert_into(attachments::table)
+            .values(self)
+            .on_conflict(attachments::id)
+            .do_update()
+            .set(self)
+            .execute(&**conn)
+            .map_res("Error saving attachment")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        return diesel::replace_into(attachments::table)
             .values(self)
             .execute(&**conn)
             .map_res("Error saving attachment")
diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs
index d4b176c4..9b7c1207 100644
--- a/src/db/models/cipher.rs
+++ b/src/db/models/cipher.rs
@@ -5,7 +5,7 @@ use super::{
     Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization,
 };
 
-#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
+#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "ciphers"]
 #[belongs_to(User, foreign_key = "user_uuid")]
 #[belongs_to(Organization, foreign_key = "organization_uuid")]
@@ -148,6 +148,21 @@ impl Cipher {
         user_uuids
     }
 
+    #[cfg(feature = "postgresql")]
+    pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        self.update_users_revision(conn);
+        self.updated_at = Utc::now().naive_utc();
+
+        diesel::insert_into(ciphers::table)
+            .values(&*self)
+            .on_conflict(ciphers::uuid)
+            .do_update()
+            .set(&*self)
+            .execute(&**conn)
+            .map_res("Error saving cipher")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
         self.update_users_revision(conn);
         self.updated_at = Utc::now().naive_utc();
diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs
index e2b58cf1..c3508c95 100644
--- a/src/db/models/collection.rs
+++ b/src/db/models/collection.rs
@@ -2,7 +2,7 @@ use serde_json::Value;
 
 use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization};
 
-#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
+#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "collections"]
 #[belongs_to(Organization, foreign_key = "org_uuid")]
 #[primary_key(uuid)]
@@ -43,6 +43,20 @@ use crate::error::MapResult;
 
 /// Database methods
 impl Collection {
+    #[cfg(feature = "postgresql")]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        self.update_users_revision(conn);
+
+        diesel::insert_into(collections::table)
+            .values(self)
+            .on_conflict(collections::uuid)
+            .do_update()
+            .set(self)
+            .execute(&**conn)
+            .map_res("Error saving collection")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
         self.update_users_revision(conn);
 
@@ -200,6 +214,24 @@ impl CollectionUser {
             .expect("Error loading users_collections")
     }
 
+    #[cfg(feature = "postgresql")]
+    pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
+        User::update_uuid_revision(&user_uuid, conn);
+
+        diesel::insert_into(users_collections::table)
+            .values((
+                users_collections::user_uuid.eq(user_uuid),
+                users_collections::collection_uuid.eq(collection_uuid),
+                users_collections::read_only.eq(read_only),
+            ))
+            .on_conflict((users_collections::user_uuid, users_collections::collection_uuid))
+            .do_update()
+            .set(users_collections::read_only.eq(read_only))
+            .execute(&**conn)
+            .map_res("Error adding user to collection")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, conn: &DbConn) -> EmptyResult {
         User::update_uuid_revision(&user_uuid, conn);
 
@@ -277,6 +309,21 @@ pub struct CollectionCipher {
 
 /// Database methods
 impl CollectionCipher {
+    #[cfg(feature = "postgresql")]
+    pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
+        Self::update_users_revision(&collection_uuid, conn);
+        diesel::insert_into(ciphers_collections::table)
+            .values((
+                ciphers_collections::cipher_uuid.eq(cipher_uuid),
+                ciphers_collections::collection_uuid.eq(collection_uuid),
+            ))
+            .on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid))
+            .do_nothing()
+            .execute(&**conn)
+            .map_res("Error adding cipher to collection")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult {
         Self::update_users_revision(&collection_uuid, conn);
         diesel::replace_into(ciphers_collections::table)
diff --git a/src/db/models/device.rs b/src/db/models/device.rs
index 83824bac..9f8506e1 100644
--- a/src/db/models/device.rs
+++ b/src/db/models/device.rs
@@ -2,7 +2,7 @@ use chrono::{NaiveDateTime, Utc};
 
 use super::User;
 
-#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
+#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "devices"]
 #[belongs_to(User, foreign_key = "user_uuid")]
 #[primary_key(uuid)]
@@ -114,6 +114,18 @@ use crate::error::MapResult;
 
 /// Database methods
 impl Device {
+    #[cfg(feature = "postgresql")]
+    pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        self.updated_at = Utc::now().naive_utc();
+
+        crate::util::retry(
+            || diesel::insert_into(devices::table).values(&*self).on_conflict(devices::uuid).do_update().set(&*self).execute(&**conn),
+            10,
+        )
+            .map_res("Error saving device")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
         self.updated_at = Utc::now().naive_utc();
 
diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs
index 142bb0c9..43611720 100644
--- a/src/db/models/folder.rs
+++ b/src/db/models/folder.rs
@@ -3,7 +3,7 @@ use serde_json::Value;
 
 use super::{Cipher, User};
 
-#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
+#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "folders"]
 #[belongs_to(User, foreign_key = "user_uuid")]
 #[primary_key(uuid)]
@@ -71,6 +71,21 @@ use crate::error::MapResult;
 
 /// Database methods
 impl Folder {
+    #[cfg(feature = "postgresql")]
+    pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        User::update_uuid_revision(&self.user_uuid, conn);
+        self.updated_at = Utc::now().naive_utc();
+
+        diesel::insert_into(folders::table)
+            .values(&*self)
+            .on_conflict(folders::uuid)
+            .do_update()
+            .set(&*self)
+            .execute(&**conn)
+            .map_res("Error saving folder")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
         User::update_uuid_revision(&self.user_uuid, conn);
         self.updated_at = Utc::now().naive_utc();
@@ -113,6 +128,17 @@ impl Folder {
 }
 
 impl FolderCipher {
+    #[cfg(feature = "postgresql")]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        diesel::insert_into(folders_ciphers::table)
+            .values(&*self)
+            .on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid))
+            .do_nothing()
+            .execute(&**conn)
+            .map_res("Error adding cipher to folder")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
         diesel::replace_into(folders_ciphers::table)
             .values(&*self)
diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs
index 2e5ec0f5..e092164e 100644
--- a/src/db/models/organization.rs
+++ b/src/db/models/organization.rs
@@ -3,7 +3,7 @@ use std::cmp::Ordering;
 
 use super::{CollectionUser, User};
 
-#[derive(Debug, Identifiable, Queryable, Insertable)]
+#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
 #[table_name = "organizations"]
 #[primary_key(uuid)]
 pub struct Organization {
@@ -12,7 +12,7 @@ pub struct Organization {
     pub billing_email: String,
 }
 
-#[derive(Debug, Identifiable, Queryable, Insertable)]
+#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
 #[table_name = "users_organizations"]
 #[primary_key(uuid)]
 pub struct UserOrganization {
@@ -213,6 +213,24 @@ use crate::error::MapResult;
 
 /// Database methods
 impl Organization {
+    #[cfg(feature = "postgresql")]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        UserOrganization::find_by_org(&self.uuid, conn)
+            .iter()
+            .for_each(|user_org| {
+                User::update_uuid_revision(&user_org.user_uuid, conn);
+            });
+
+        diesel::insert_into(organizations::table)
+            .values(self)
+            .on_conflict(organizations::uuid)
+            .do_update()
+            .set(self)
+            .execute(&**conn)
+            .map_res("Error saving organization")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
         UserOrganization::find_by_org(&self.uuid, conn)
             .iter()
@@ -323,6 +341,20 @@ impl UserOrganization {
         })
     }
 
+    #[cfg(feature = "postgresql")]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        User::update_uuid_revision(&self.user_uuid, conn);
+
+        diesel::insert_into(users_organizations::table)
+            .values(self)
+            .on_conflict(users_organizations::uuid)
+            .do_update()
+            .set(self)
+            .execute(&**conn)
+            .map_res("Error adding user to organization")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
         User::update_uuid_revision(&self.user_uuid, conn);
 
diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs
index 233d5312..d869245c 100644
--- a/src/db/models/two_factor.rs
+++ b/src/db/models/two_factor.rs
@@ -9,7 +9,7 @@ use crate::error::MapResult;
 
 use super::User;
 
-#[derive(Debug, Identifiable, Queryable, Insertable, Associations)]
+#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)]
 #[table_name = "twofactor"]
 #[belongs_to(User, foreign_key = "user_uuid")]
 #[primary_key(uuid)]
@@ -69,6 +69,18 @@ impl TwoFactor {
 
 /// Database methods
 impl TwoFactor {
+    #[cfg(feature = "postgresql")]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        diesel::insert_into(twofactor::table)
+            .values(self)
+            .on_conflict(twofactor::uuid)
+            .do_update()
+            .set(self)
+            .execute(&**conn)
+            .map_res("Error saving twofactor")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
         diesel::replace_into(twofactor::table)
             .values(self)
diff --git a/src/db/models/user.rs b/src/db/models/user.rs
index 4306889b..f35739e1 100644
--- a/src/db/models/user.rs
+++ b/src/db/models/user.rs
@@ -4,7 +4,7 @@ use serde_json::Value;
 use crate::crypto;
 use crate::CONFIG;
 
-#[derive(Debug, Identifiable, Queryable, Insertable)]
+#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)]
 #[table_name = "users"]
 #[primary_key(uuid)]
 pub struct User {
@@ -148,6 +148,24 @@ impl User {
         })
     }
 
+    #[cfg(feature = "postgresql")]
+    pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
+        if self.email.trim().is_empty() {
+            err!("User email can't be empty")
+        }
+
+        self.updated_at = Utc::now().naive_utc();
+
+        diesel::insert_into(users::table) // Insert or update
+            .values(&*self)
+            .on_conflict(users::uuid)
+            .do_update()
+            .set(&*self)
+            .execute(&**conn)
+            .map_res("Error saving user")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&mut self, conn: &DbConn) -> EmptyResult {
         if self.email.trim().is_empty() {
             err!("User email can't be empty")
@@ -250,6 +268,21 @@ impl Invitation {
         Self { email }
     }
 
+    #[cfg(feature = "postgresql")]
+    pub fn save(&self, conn: &DbConn) -> EmptyResult {
+        if self.email.trim().is_empty() {
+            err!("Invitation email can't be empty")
+        }
+
+        diesel::insert_into(invitations::table)
+            .values(self)
+            .on_conflict(invitations::email)
+            .do_nothing()
+            .execute(&**conn)
+            .map_res("Error saving invitation")
+    }
+
+    #[cfg(not(feature = "postgresql"))]
     pub fn save(&self, conn: &DbConn) -> EmptyResult {
         if self.email.trim().is_empty() {
             err!("Invitation email can't be empty")
diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs
new file mode 100644
index 00000000..1bc924c9
--- /dev/null
+++ b/src/db/schemas/postgresql/schema.rs
@@ -0,0 +1,172 @@
+table! {
+    attachments (id) {
+        id -> Text,
+        cipher_uuid -> Text,
+        file_name -> Text,
+        file_size -> Integer,
+        akey -> Nullable<Text>,
+    }
+}
+
+table! {
+    ciphers (uuid) {
+        uuid -> Text,
+        created_at -> Timestamp,
+        updated_at -> Timestamp,
+        user_uuid -> Nullable<Text>,
+        organization_uuid -> Nullable<Text>,
+        atype -> Integer,
+        name -> Text,
+        notes -> Nullable<Text>,
+        fields -> Nullable<Text>,
+        data -> Text,
+        favorite -> Bool,
+        password_history -> Nullable<Text>,
+    }
+}
+
+table! {
+    ciphers_collections (cipher_uuid, collection_uuid) {
+        cipher_uuid -> Text,
+        collection_uuid -> Text,
+    }
+}
+
+table! {
+    collections (uuid) {
+        uuid -> Text,
+        org_uuid -> Text,
+        name -> Text,
+    }
+}
+
+table! {
+    devices (uuid) {
+        uuid -> Text,
+        created_at -> Timestamp,
+        updated_at -> Timestamp,
+        user_uuid -> Text,
+        name -> Text,
+        atype -> Integer,
+        push_token -> Nullable<Text>,
+        refresh_token -> Text,
+        twofactor_remember -> Nullable<Text>,
+    }
+}
+
+table! {
+    folders (uuid) {
+        uuid -> Text,
+        created_at -> Timestamp,
+        updated_at -> Timestamp,
+        user_uuid -> Text,
+        name -> Text,
+    }
+}
+
+table! {
+    folders_ciphers (cipher_uuid, folder_uuid) {
+        cipher_uuid -> Text,
+        folder_uuid -> Text,
+    }
+}
+
+table! {
+    invitations (email) {
+        email -> Text,
+    }
+}
+
+table! {
+    organizations (uuid) {
+        uuid -> Text,
+        name -> Text,
+        billing_email -> Text,
+    }
+}
+
+table! {
+    twofactor (uuid) {
+        uuid -> Text,
+        user_uuid -> Text,
+        atype -> Integer,
+        enabled -> Bool,
+        data -> Text,
+    }
+}
+
+table! {
+    users (uuid) {
+        uuid -> Text,
+        created_at -> Timestamp,
+        updated_at -> Timestamp,
+        email -> Text,
+        name -> Text,
+        password_hash -> Binary,
+        salt -> Binary,
+        password_iterations -> Integer,
+        password_hint -> Nullable<Text>,
+        akey -> Text,
+        private_key -> Nullable<Text>,
+        public_key -> Nullable<Text>,
+        totp_secret -> Nullable<Text>,
+        totp_recover -> Nullable<Text>,
+        security_stamp -> Text,
+        equivalent_domains -> Text,
+        excluded_globals -> Text,
+        client_kdf_type -> Integer,
+        client_kdf_iter -> Integer,
+    }
+}
+
+table! {
+    users_collections (user_uuid, collection_uuid) {
+        user_uuid -> Text,
+        collection_uuid -> Text,
+        read_only -> Bool,
+    }
+}
+
+table! {
+    users_organizations (uuid) {
+        uuid -> Text,
+        user_uuid -> Text,
+        org_uuid -> Text,
+        access_all -> Bool,
+        akey -> Text,
+        status -> Integer,
+        atype -> Integer,
+    }
+}
+
+joinable!(attachments -> ciphers (cipher_uuid));
+joinable!(ciphers -> organizations (organization_uuid));
+joinable!(ciphers -> users (user_uuid));
+joinable!(ciphers_collections -> ciphers (cipher_uuid));
+joinable!(ciphers_collections -> collections (collection_uuid));
+joinable!(collections -> organizations (org_uuid));
+joinable!(devices -> users (user_uuid));
+joinable!(folders -> users (user_uuid));
+joinable!(folders_ciphers -> ciphers (cipher_uuid));
+joinable!(folders_ciphers -> folders (folder_uuid));
+joinable!(twofactor -> users (user_uuid));
+joinable!(users_collections -> collections (collection_uuid));
+joinable!(users_collections -> users (user_uuid));
+joinable!(users_organizations -> organizations (org_uuid));
+joinable!(users_organizations -> users (user_uuid));
+
+allow_tables_to_appear_in_same_query!(
+    attachments,
+    ciphers,
+    ciphers_collections,
+    collections,
+    devices,
+    folders,
+    folders_ciphers,
+    invitations,
+    organizations,
+    twofactor,
+    users,
+    users_collections,
+    users_organizations,
+);
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 081997dc..384d6c14 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,8 @@
 #![feature(proc_macro_hygiene, decl_macro, vec_remove_item, try_trait)]
 #![recursion_limit = "256"]
 
+#[cfg(feature = "openssl")]
+extern crate openssl;
 #[macro_use]
 extern crate rocket;
 #[macro_use]
@@ -215,6 +217,8 @@ mod migrations {
     embed_migrations!("migrations/sqlite");
     #[cfg(feature = "mysql")]
     embed_migrations!("migrations/mysql");
+    #[cfg(feature = "postgresql")]
+    embed_migrations!("migrations/postgresql");
 
     pub fn run_migrations() {
         // Make sure the database is up to date (create if it doesn't exist, or run the migrations)