diff --git a/migrations/2020-12-22-183017_addProgramPublishedFlag/down.sql b/migrations/2020-12-22-183017_addProgramPublishedFlag/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..1aa5247d9a29707cfa1b1b2dd5a862a0d0b198d4
--- /dev/null
+++ b/migrations/2020-12-22-183017_addProgramPublishedFlag/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE user_programs DROP COLUMN published;
\ No newline at end of file
diff --git a/migrations/2020-12-22-183017_addProgramPublishedFlag/up.sql b/migrations/2020-12-22-183017_addProgramPublishedFlag/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..6dea36ddc1e236873a44e2be026864aef3b81dd8
--- /dev/null
+++ b/migrations/2020-12-22-183017_addProgramPublishedFlag/up.sql
@@ -0,0 +1 @@
+ALTER TABLE user_programs ADD COLUMN published INT NOT NULL DEFAULT 0;
\ No newline at end of file
diff --git a/src/handler.rs b/src/handler.rs
index 699a1ea1f44f02dc47db0c1625a76961866b8972..e79997c4e7f67d4cb9a2ca42e157c8e83d558d83 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -8,7 +8,7 @@ use serenity::model::channel::{Message, ReactionType};
 use serenity::model::id::UserId;
 use serenity::model::prelude::Ready;
 use serenity::prelude::*;
-use std::borrow::Cow;
+use std::borrow::{Borrow, Cow};
 use std::collections::HashMap;
 use std::str::FromStr;
 use std::sync::{Arc, Mutex};
@@ -86,6 +86,25 @@ impl Handler {
 				"LIST" => {
 					self.interpreter_list(msg, ctx).await;
 				}
+				"DIR" => {
+					if self.list_saved_programs(msg, ctx).await.is_none() {
+						msg.channel_id
+							.say(&ctx, "Could not get list of programs from database.")
+							.await
+							.unwrap();
+					}
+				}
+				"PUBDIR" => {
+					if self.list_published_programs(msg, ctx).await.is_none() {
+						msg.channel_id
+							.say(
+								&ctx,
+								"Could not get list of published programs from database.",
+							)
+							.await
+							.unwrap();
+					}
+				}
 				_ => {
 					if let Some(name) = line.strip_prefix("SAVE ") {
 						self.interpreter_save(name, msg, ctx).await;
@@ -95,6 +114,43 @@ impl Handler {
 						self.interpreter_load(name, msg, ctx).await;
 					}
 
+					if let Some(name) = line.strip_prefix("PUB ") {
+						if self.publish_program(name, msg, ctx).await.is_none() {
+							msg.channel_id
+								.say(&ctx, format!("Could not publish {}.", name))
+								.await
+								.unwrap();
+						}
+					}
+
+					if let Some(name) = line.strip_prefix("UNPUB ") {
+						if self.unpublish_program(name, msg, ctx).await.is_none() {
+							msg.channel_id
+								.say(&ctx, &format!("Could not unpublish {}.", name))
+								.await
+								.unwrap();
+						}
+					}
+
+					if let Some(id_str) = line.strip_prefix("PUBLOAD ") {
+						match id_str.parse::<i32>() {
+							Ok(id) => {
+								if self.load_published_program(msg, ctx, id).await.is_none() {
+									msg.channel_id
+										.say(&ctx, format!("Could not load {}.", id))
+										.await
+										.unwrap();
+								}
+							}
+							Err(_) => {
+								msg.channel_id
+									.say(&ctx, "PUBLOAD requires a numerical ID.")
+									.await
+									.unwrap();
+							}
+						}
+					}
+
 					let mut split = line.splitn(2, ' ');
 					let first = split.next().unwrap();
 					if let Ok(num) = first.parse::<u32>() {
@@ -139,6 +195,84 @@ impl Handler {
 			.insert(msg.author.id, Program::new());
 	}
 
+	async fn list_saved_programs(&self, msg: &Message, ctx: &Context) -> Option<()> {
+		let program_names =
+			Program::list_programs_by_user(self.conn.lock().ok()?.borrow(), msg.author.id)?;
+		msg.channel_id
+			.say(
+				&ctx,
+				format!(
+					"You have {} programs saved:\r\n```\r\n{}\r\n```",
+					program_names.len(),
+					program_names.join("\r\n")
+				),
+			)
+			.await
+			.ok()?;
+
+		Some(())
+	}
+
+	async fn list_published_programs(&self, msg: &Message, ctx: &Context) -> Option<()> {
+		let program_names: Vec<String> =
+			Program::list_published_programs(self.conn.lock().ok()?.borrow())?
+				.iter()
+				.map(|row| format!("{}\t{}", row.0, row.1))
+				.collect();
+
+		msg.channel_id
+			.say(
+				&ctx,
+				format!(
+				"Found {} public programs:\r\n```\r\nid\tname\r\n{}\r\n```\r\n Load one with `PUBLOAD <id>`",
+					program_names.len(),
+				program_names.join("\r\n")),
+			)
+			.await
+			.unwrap();
+
+		Some(())
+	}
+
+	async fn publish_program(&self, name: &str, msg: &Message, ctx: &Context) -> Option<()> {
+		Program::set_program_published(self.conn.lock().ok()?.borrow(), name, msg.author.id, true)?;
+
+		msg.channel_id
+			.say(&ctx, format!("Published {}.", name))
+			.await
+			.unwrap();
+
+		Some(())
+	}
+
+	async fn unpublish_program(&self, name: &str, msg: &Message, ctx: &Context) -> Option<()> {
+		Program::set_program_published(
+			self.conn.lock().ok()?.borrow(),
+			name,
+			msg.author.id,
+			false,
+		)?;
+
+		msg.channel_id
+			.say(&ctx, format!("Published {}.", name))
+			.await
+			.unwrap();
+
+		Some(())
+	}
+
+	async fn load_published_program(&self, msg: &Message, ctx: &Context, id: i32) -> Option<()> {
+		let name = get_user_programs!(&self, &msg.author.id)
+			.load_published_program(&self.conn.lock().ok()?.borrow(), id)?;
+
+		msg.channel_id
+			.say(&ctx, format!("Loaded {} (\"{}\") into memory.", id, name))
+			.await
+			.unwrap();
+
+		Some(())
+	}
+
 	async fn interpreter_list(&self, msg: &Message, ctx: &Context) {
 		msg.channel_id
 			.say(
diff --git a/src/program.rs b/src/program.rs
index dd4a7959dfa1792604ec1de66649fdd3b9bad699..689e8e81499b51aa5cb5d44a7b7a2ee39878ba70 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -18,6 +18,65 @@ impl Program {
 		}
 	}
 
+	pub fn list_programs_by_user(conn: &PgConnection, user_id: UserId) -> Option<Vec<String>> {
+		user_programs
+			.filter(columns::discord_user_id.eq(BigDecimal::from_u64(*user_id.as_u64()).unwrap()))
+			.select(columns::name)
+			.get_results(conn)
+			.ok()
+	}
+
+	pub fn list_published_programs(conn: &PgConnection) -> Option<Vec<(i32, String)>> {
+		user_programs
+			.filter(columns::published.eq(1))
+			.select((columns::id, columns::name))
+			.load(conn)
+			.ok()
+	}
+
+	pub fn load_published_program(&mut self, conn: &PgConnection, id: i32) -> Option<String> {
+		let program: Vec<(String, String)> = user_programs
+			.filter(columns::id.eq(id).and(columns::published.eq(1)))
+			.limit(1)
+			.select((columns::name, columns::code))
+			.get_results(conn)
+			.ok()?;
+
+		if program.is_empty() {
+			return None;
+		}
+
+		let name = &program[0].0;
+		let code = &program[0].1;
+
+		self.parse_string(code)?;
+
+		Some(name.to_string())
+	}
+
+	pub fn set_program_published(
+		conn: &PgConnection,
+		name: &str,
+		user_id: UserId,
+		published: bool,
+	) -> Option<()> {
+		if diesel::update(
+			user_programs.filter(
+				columns::discord_user_id
+					.eq(BigDecimal::from_u64(*user_id.as_u64()).unwrap())
+					.and(columns::name.eq(name)),
+			),
+		)
+		.set(columns::published.eq(if published { 1 } else { 0 }))
+		.execute(conn)
+		.ok()? == 1
+		{
+			Some(())
+		} else {
+			None
+		}
+	}
+
 	pub fn stringify(&self) -> String {
 		let mut code: Vec<(u32, String)> =
 			self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect();
diff --git a/src/schema.rs b/src/schema.rs
index 115cafc6b9e8e554f49404145309ebb6916b5957..3278e43014c6a352eb533a370c60b35245b43e10 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -1,8 +1,9 @@
 table! {
-    user_programs (id) {
-        id -> Int4,
-        discord_user_id -> Numeric,
-        name -> Varchar,
-        code -> Text,
-    }
+	user_programs (id) {
+		id -> Int4,
+		discord_user_id -> Numeric,
+		name -> Varchar,
+		code -> Text,
+		published -> Int4,
+	}
 }