diff --git a/Cargo.lock b/Cargo.lock
index 5480247c3b4b6cb8387bede7af1dddb83cdb5230..ceef6b8c68cbd996c4293a2b832445484053ebc5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -14,6 +14,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
 
+[[package]]
+name = "anyhow"
+version = "1.0.65"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
+
 [[package]]
 name = "arrayvec"
 version = "0.4.12"
@@ -116,6 +122,7 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
 name = "cat-disruptor-6500"
 version = "0.1.2"
 dependencies = [
+ "anyhow",
  "bigdecimal",
  "diesel",
  "dotenv",
diff --git a/Cargo.toml b/Cargo.toml
index ab17f677b2a8f70ee4d6fac812e8a59646834925..7a64ced8488831fd3e27e1b1adf55e09daa5d693 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,3 +22,4 @@ serde_json = "1.0"
 rusttype = "0.4.3"
 rand = "0.8"
 itertools = "0.10"
+anyhow = "1.0"
diff --git a/migrations/2022-10-05-220626_server_settings/down.sql b/migrations/2022-10-05-220626_server_settings/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..7cc9fced19fb1c38ed0a78ad551fcec7171ba0b9
--- /dev/null
+++ b/migrations/2022-10-05-220626_server_settings/down.sql
@@ -0,0 +1 @@
+DROP TABLE server_settings;
diff --git a/migrations/2022-10-05-220626_server_settings/up.sql b/migrations/2022-10-05-220626_server_settings/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..0f6908906d4ffb80cacbb1d728afa3bebc08bb68
--- /dev/null
+++ b/migrations/2022-10-05-220626_server_settings/up.sql
@@ -0,0 +1,7 @@
+CREATE TABLE server_settings (
+	   id SERIAL PRIMARY KEY NOT NULL,
+	   guild_id NUMERIC NOT NULL UNIQUE,
+	   starboard_threshold INTEGER NOT NULL,
+	   starboard_emoji_id NUMERIC NOT NULL,
+	   starboard_channel NUMERIC NOT NULL
+);
diff --git a/migrations/2022-10-06-221533_starboard_mapping/down.sql b/migrations/2022-10-06-221533_starboard_mapping/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..bad20246c7414330c6069c180c84250c60173ac4
--- /dev/null
+++ b/migrations/2022-10-06-221533_starboard_mapping/down.sql
@@ -0,0 +1 @@
+DROP TABLE starboard_mappings;
diff --git a/migrations/2022-10-06-221533_starboard_mapping/up.sql b/migrations/2022-10-06-221533_starboard_mapping/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..82e7f1606f5a1f01bcd6398b27180e59bb6477f2
--- /dev/null
+++ b/migrations/2022-10-06-221533_starboard_mapping/up.sql
@@ -0,0 +1,4 @@
+CREATE TABLE starboard_mappings (
+	   original_id NUMERIC NOT NULL UNIQUE PRIMARY KEY,
+	   repost_id NUMERIC UNIQUE
+);
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index d1c71e30621e825ce11299bcdbb1719ac3df069c..7df01df2c768f9e9c2de25e919eb4c4da861adbe 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -1,12 +1,14 @@
 mod horse;
 mod joke;
 mod react;
+mod starboard;
 mod sus;
 mod xbasic;
 
 use crate::handlers::horse::HorseHandler;
 use crate::handlers::joke::*;
 use crate::handlers::react::*;
+use crate::handlers::starboard::StarboardHandler;
 use crate::handlers::sus::*;
 use crate::handlers::xbasic::*;
 
@@ -14,7 +16,7 @@ use diesel::{Connection, PgConnection};
 use dotenv::dotenv;
 use serenity::async_trait;
 use serenity::model::channel::Message;
-use serenity::model::prelude::Ready;
+use serenity::model::prelude::{Reaction, Ready};
 use serenity::prelude::*;
 use std::env;
 use std::sync::Arc;
@@ -31,8 +33,14 @@ pub(crate) trait LineHandler: Send + Sync {
 	async fn line(&self, _ctx: &Context, _msg: &Message, _line: &str) {}
 }
 
+#[async_trait]
+pub(crate) trait ReactionHandler: Send + Sync {
+	async fn reaction(&self, _ctx: &Context, reaction: &Reaction);
+}
+
 pub(crate) struct Dispatcher {
 	handlers: Vec<Box<dyn LineHandler>>,
+	reacts: Vec<Box<dyn ReactionHandler>>,
 }
 
 #[async_trait]
@@ -43,6 +51,12 @@ impl EventHandler for Dispatcher {
 		}
 	}
 
+	async fn reaction_add(&self, ctx: Context, reaction: Reaction) {
+		for r in &self.reacts {
+			r.reaction(&ctx, &reaction).await;
+		}
+	}
+
 	async fn ready(&self, _: Context, ready: Ready) {
 		println!("{} is connected", ready.user.name);
 	}
@@ -54,12 +68,13 @@ impl Default for Dispatcher {
 
 		Self {
 			handlers: vec![
-				Box::new(XbasicHandler::new(conn)),
+				Box::new(XbasicHandler::new(conn.clone())),
 				Box::new(JokeHandler::default()),
 				Box::new(ReactHandler::default()),
 				Box::new(SusHandler::default()),
 				Box::new(HorseHandler::default()),
 			],
+			reacts: vec![Box::new(StarboardHandler::new(conn))],
 		}
 	}
 }
diff --git a/src/handlers/starboard.rs b/src/handlers/starboard.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e19d07707a7d3993044df15ba640acd8a3c70f4c
--- /dev/null
+++ b/src/handlers/starboard.rs
@@ -0,0 +1,137 @@
+use crate::handlers::ReactionHandler;
+use crate::models::ServerSetting;
+use crate::schema;
+use crate::schema::{server_settings, starboard_mappings};
+
+use anyhow::Context as AnyhowContext;
+use bigdecimal::{BigDecimal, FromPrimitive, ToPrimitive};
+use diesel::{BoolExpressionMethods, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl};
+use serenity::async_trait;
+use serenity::model::prelude::{
+	ChannelId, Embed, EmojiId, GuildChannel, GuildId, Message, Reaction, ReactionType,
+};
+use serenity::prelude::*;
+use std::sync::Arc;
+use tokio::sync::Mutex;
+
+pub struct StarboardHandler {
+	conn: Arc<Mutex<PgConnection>>,
+}
+
+impl StarboardHandler {
+	pub fn new(conn: Arc<Mutex<PgConnection>>) -> Self {
+		Self { conn }
+	}
+
+	async fn handle_reaction(&self, ctx: &Context, reaction: &Reaction) -> anyhow::Result<()> {
+		let guild = match reaction.channel_id.to_channel(ctx).await?.guild() {
+			Some(x) => x,
+			None => return Ok(()),
+		};
+
+		// get corresponding guild settings
+		let gs: Vec<ServerSetting> = server_settings::dsl::server_settings
+			.filter(
+				schema::server_settings::columns::guild_id
+					.eq(BigDecimal::from_u64(guild.guild_id.0).unwrap()),
+			)
+			.limit(1)
+			.get_results(&*self.conn.lock().await)?;
+		let gs = match gs.first() {
+			Some(x) => x,
+			None => return Ok(()),
+		};
+		let emoji = EmojiId(
+			gs.starboard_emoji_id
+				.to_u64()
+				.context("Could not convert emoji id to u64")?,
+		);
+
+		if Self::emoji_match(&reaction.emoji, emoji) {
+			let msg = reaction.message(ctx).await?;
+			for mr in &msg.reactions {
+				if Self::emoji_match(&mr.reaction_type, emoji) {
+					self.handle_matching_reaction(ctx, gs, mr.count, &msg, &guild.guild_id)
+						.await?;
+					break;
+				}
+			}
+		}
+
+		Ok(())
+	}
+
+	async fn handle_matching_reaction(
+		&self,
+		ctx: &Context,
+		gs: &ServerSetting,
+		count: u64,
+		msg: &Message,
+		guild: &GuildId,
+	) -> anyhow::Result<()> {
+		if count >= gs.starboard_threshold as u64 {
+			let original_id = BigDecimal::from(msg.id.0);
+			diesel::insert_into(starboard_mappings::dsl::starboard_mappings)
+				.values(starboard_mappings::columns::original_id.eq(&original_id))
+				.returning(starboard_mappings::columns::repost_id)
+				.on_conflict_do_nothing()
+				.execute(&*self.conn.lock().await)?;
+
+			let repost_id = starboard_mappings::dsl::starboard_mappings
+				.filter(starboard_mappings::columns::original_id.eq(&original_id))
+				.select(starboard_mappings::columns::repost_id)
+				.limit(1)
+				.get_results(&*self.conn.lock().await)?;
+
+			let repost_id: &Option<BigDecimal> =
+				repost_id.get(0).context("Insert of mapping failed")?;
+
+			if repost_id.is_none() {
+				// post to repost channel
+				let name = msg
+					.author
+					.nick_in(&ctx, guild)
+					.await
+					.unwrap_or_else(|| msg.author.tag());
+
+				let repost = ChannelId(
+					gs.starboard_channel
+						.to_u64()
+						.context("Could not convert starboard channel to a u64")?,
+				)
+				.send_message(ctx, |m| {
+					m.embed(|e| {
+						e.description(format!("[Jump to source]({})\n{}", msg.link(), msg.content))
+							.author(|a| a.name(&name).icon_url(msg.author.face()))
+							.timestamp(&msg.timestamp)
+					})
+				})
+				.await?;
+
+				// update the DB
+				let repost_id = BigDecimal::from_u64(repost.id.0);
+				diesel::update(
+					starboard_mappings::dsl::starboard_mappings
+						.filter(starboard_mappings::columns::original_id.eq(original_id)),
+				)
+				.set(starboard_mappings::columns::repost_id.eq(repost_id))
+				.execute(&*self.conn.lock().await)?;
+			}
+		}
+
+		Ok(())
+	}
+
+	fn emoji_match(rt: &ReactionType, em: EmojiId) -> bool {
+		matches!(rt, ReactionType::Custom { id, .. } if *id == em)
+	}
+}
+
+#[async_trait]
+impl ReactionHandler for StarboardHandler {
+	async fn reaction(&self, ctx: &Context, reaction: &Reaction) {
+		if let Err(e) = self.handle_reaction(ctx, reaction).await {
+			eprintln!("Error in starboard: {:?}", e);
+		}
+	}
+}
diff --git a/src/handlers/xbasic.rs b/src/handlers/xbasic.rs
index fe3b27d1567552a8f16a291f2f62f82d52b1fa5c..efae2bd2cf92b7be3e85e559dd2d4b602955d4ee 100644
--- a/src/handlers/xbasic.rs
+++ b/src/handlers/xbasic.rs
@@ -1,6 +1,7 @@
 use crate::framebuffer::FrameBuffer;
 use crate::handlers::LineHandler;
 use crate::program::Program;
+
 use diesel::PgConnection;
 use serenity::async_trait;
 use serenity::http::AttachmentType;
diff --git a/src/models.rs b/src/models.rs
index 0b7f70519457ae868c5e23eb765b968a0f8b3ca1..10d32fea3696c52717ba4eb92d647342a6c39ac4 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -16,3 +16,12 @@ pub struct NewUserProgram<'a> {
 	pub name: &'a str,
 	pub code: &'a str,
 }
+
+#[derive(Queryable)]
+pub struct ServerSetting {
+	pub id: i32,
+	pub guild_id: BigDecimal,
+	pub starboard_threshold: i32,
+	pub starboard_emoji_id: BigDecimal,
+	pub starboard_channel: BigDecimal,
+}
diff --git a/src/schema.rs b/src/schema.rs
index 3278e43014c6a352eb533a370c60b35245b43e10..4a883c63425e0e2a5e64207792079a9764683d35 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -1,9 +1,32 @@
 table! {
-	user_programs (id) {
-		id -> Int4,
-		discord_user_id -> Numeric,
-		name -> Varchar,
-		code -> Text,
-		published -> Int4,
-	}
+    server_settings (id) {
+        id -> Int4,
+        guild_id -> Numeric,
+        starboard_threshold -> Int4,
+        starboard_emoji_id -> Numeric,
+        starboard_channel -> Numeric,
+    }
 }
+
+table! {
+    starboard_mappings (original_id) {
+        original_id -> Numeric,
+        repost_id -> Nullable<Numeric>,
+    }
+}
+
+table! {
+    user_programs (id) {
+        id -> Int4,
+        discord_user_id -> Numeric,
+        name -> Varchar,
+        code -> Text,
+        published -> Int4,
+    }
+}
+
+allow_tables_to_appear_in_same_query!(
+    server_settings,
+    starboard_mappings,
+    user_programs,
+);