diff --git a/.env b/.env
new file mode 100644
index 0000000000000000000000000000000000000000..f7487d118966c7f41b68b94420fd7d732b3be5dc
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+DATABASE_URL=postgres://stephen@localhost/cat_disruptor_6500
diff --git a/Cargo.lock b/Cargo.lock
index aaa391a32f8c379949f4d029b1e9889cc89a0a3e..81336a360473070a641bf27adfa849305a741fea 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -57,6 +57,17 @@ version = "0.13.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
 
+[[package]]
+name = "bigdecimal"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1374191e2dd25f9ae02e3aa95041ed5d747fc77b3c102b49fe2dd9a8117a6244"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "bitflags"
 version = "1.2.1"
@@ -94,6 +105,9 @@ checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38"
 name = "cat-disruptor-6500"
 version = "0.1.0"
 dependencies = [
+ "bigdecimal",
+ "diesel",
+ "dotenv",
  "phf",
  "png",
  "serde",
@@ -160,6 +174,33 @@ dependencies = [
  "byteorder",
 ]
 
+[[package]]
+name = "diesel"
+version = "1.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2de9deab977a153492a1468d1b1c0662c1cf39e5ea87d0c060ecd59ef18d8c"
+dependencies = [
+ "bigdecimal",
+ "bitflags",
+ "byteorder",
+ "diesel_derives",
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+ "pq-sys",
+]
+
+[[package]]
+name = "diesel_derives"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.7",
+ "syn 1.0.48",
+]
+
 [[package]]
 name = "digest"
 version = "0.9.0"
@@ -169,6 +210,12 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
 [[package]]
 name = "dtoa"
 version = "0.4.6"
@@ -643,6 +690,17 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "num-bigint"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.2.5"
@@ -815,6 +873,15 @@ version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20"
 
+[[package]]
+name = "pq-sys"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ac25eee5a0582f45a67e837e350d784e7003bd29a5f460796772061ca49ffda"
+dependencies = [
+ "vcpkg",
+]
+
 [[package]]
 name = "proc-macro-hack"
 version = "0.5.19"
@@ -1384,6 +1451,12 @@ version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7"
 
+[[package]]
+name = "vcpkg"
+version = "0.2.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
+
 [[package]]
 name = "version_check"
 version = "0.9.2"
@@ -1574,6 +1647,8 @@ dependencies = [
 [[package]]
 name = "xbasic"
 version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1521d9f71cc07a276b915ea6b670b06178cf84caa82636a9f5f1c4a505d1fe90"
 dependencies = [
  "num-derive",
  "num-traits",
diff --git a/Cargo.toml b/Cargo.toml
index a49a643f433827cfed1813993b9b7bbfd7f3f03a..e735bb8bd81af7920ef2272e309cbf0fde61e704 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -12,6 +12,8 @@ tokio = {version = "0.2", features = ["full", "time"] }
 phf = { version = "0.8", features = ["macros"] }
 toml = "0.5"
 serde = { version = "1.0", features = ["derive"] }
-# xbasic = "0.2"
-xbasic = { path = "../xbasic", version = "0.2.0" }
-png = "0.16"
\ No newline at end of file
+xbasic = "0.2"
+png = "0.16"
+diesel = { version = "1.4", features = ["postgres", "numeric"] }
+dotenv = "0.15.0"
+bigdecimal = "0.1.2"
\ No newline at end of file
diff --git a/diesel.toml b/diesel.toml
new file mode 100644
index 0000000000000000000000000000000000000000..92267c829f2027cdf0e641efa95622d7ddd8bb74
--- /dev/null
+++ b/diesel.toml
@@ -0,0 +1,5 @@
+# For documentation on how to configure this file,
+# see diesel.rs/guides/configuring-diesel-cli
+
+[print_schema]
+file = "src/schema.rs"
diff --git a/migrations/.gitkeep b/migrations/.gitkeep
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/migrations/00000000000000_diesel_initial_setup/down.sql b/migrations/00000000000000_diesel_initial_setup/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..a9f526091194b70e312bd1b30084ea34e0df9670
--- /dev/null
+++ b/migrations/00000000000000_diesel_initial_setup/down.sql
@@ -0,0 +1,6 @@
+-- This file was automatically created by Diesel to setup helper functions
+-- and other internal bookkeeping. This file is safe to edit, any future
+-- changes will be added to existing projects as new migrations.
+
+DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
+DROP FUNCTION IF EXISTS diesel_set_updated_at();
diff --git a/migrations/00000000000000_diesel_initial_setup/up.sql b/migrations/00000000000000_diesel_initial_setup/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..d68895b1a7b7dbbd699eb627b23b01746eb80222
--- /dev/null
+++ b/migrations/00000000000000_diesel_initial_setup/up.sql
@@ -0,0 +1,36 @@
+-- This file was automatically created by Diesel to setup helper functions
+-- and other internal bookkeeping. This file is safe to edit, any future
+-- changes will be added to existing projects as new migrations.
+
+
+
+
+-- Sets up a trigger for the given table to automatically set a column called
+-- `updated_at` whenever the row is modified (unless `updated_at` was included
+-- in the modified columns)
+--
+-- # Example
+--
+-- ```sql
+-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
+--
+-- SELECT diesel_manage_updated_at('users');
+-- ```
+CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
+BEGIN
+    EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
+                    FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
+BEGIN
+    IF (
+        NEW IS DISTINCT FROM OLD AND
+        NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
+    ) THEN
+        NEW.updated_at := current_timestamp;
+    END IF;
+    RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
diff --git a/migrations/2020-11-21-032604_create_user_programs/down.sql b/migrations/2020-11-21-032604_create_user_programs/down.sql
new file mode 100644
index 0000000000000000000000000000000000000000..7bc403778ba99b6112148abb205225e773ae35ee
--- /dev/null
+++ b/migrations/2020-11-21-032604_create_user_programs/down.sql
@@ -0,0 +1 @@
+DROP TABLE user_programs
\ No newline at end of file
diff --git a/migrations/2020-11-21-032604_create_user_programs/up.sql b/migrations/2020-11-21-032604_create_user_programs/up.sql
new file mode 100644
index 0000000000000000000000000000000000000000..8810af33a03236e82f5673893ec88a707bfa1dd5
--- /dev/null
+++ b/migrations/2020-11-21-032604_create_user_programs/up.sql
@@ -0,0 +1,11 @@
+CREATE TABLE user_programs (
+id SERIAL PRIMARY KEY,
+discord_user_id NUMERIC NOT NULL,
+name VARCHAR NOT NULL,
+code TEXT NOT NULL
+);
+
+CREATE UNIQUE INDEX user_programs_name_id_index ON user_programs (
+discord_user_id,
+name
+);
\ No newline at end of file
diff --git a/src/framebuffer.rs b/src/framebuffer.rs
new file mode 100644
index 0000000000000000000000000000000000000000..034555b29ee65a1669106b138ef68c4301104c86
--- /dev/null
+++ b/src/framebuffer.rs
@@ -0,0 +1,43 @@
+#[derive(Clone)]
+pub(crate) struct FrameBuffer {
+	width: u32,
+	height: u32,
+	buffer: Vec<u8>,
+}
+
+impl FrameBuffer {
+	pub fn new(width: u32, height: u32) -> Self {
+		Self {
+			width,
+			height,
+			buffer: vec![0; width as usize * height as usize * 4],
+		}
+	}
+
+	pub fn set_pixel(&mut self, x: u32, y: u32, red: u8, green: u8, blue: u8, alpha: u8) {
+		if x >= self.width || y >= self.height {
+			return;
+		}
+
+		let x = x as usize;
+		let y = y as usize;
+		self.buffer[(x + self.width as usize * y) * 4] = red;
+		self.buffer[(x + self.width as usize * y) * 4 + 1] = green;
+		self.buffer[(x + self.width as usize * y) * 4 + 2] = blue;
+		self.buffer[(x + self.width as usize * y) * 4 + 3] = alpha;
+	}
+
+	pub fn as_png_vec(&self) -> Vec<u8> {
+		let mut buffer: Vec<u8> = Vec::new();
+
+		{
+			let mut encoder = png::Encoder::new(&mut buffer, self.width, self.height);
+			encoder.set_color(png::ColorType::RGBA);
+			encoder.set_depth(png::BitDepth::Eight);
+			let mut writer = encoder.write_header().unwrap();
+			writer.write_image_data(&self.buffer).unwrap();
+		}
+
+		buffer
+	}
+}
diff --git a/src/handler.rs b/src/handler.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ffd662e11280f74656e5fbf8bd8460e7b4655795
--- /dev/null
+++ b/src/handler.rs
@@ -0,0 +1,291 @@
+use crate::framebuffer::FrameBuffer;
+use crate::program::Program;
+use diesel::PgConnection;
+use phf::phf_map;
+use serenity::async_trait;
+use serenity::http::AttachmentType;
+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::collections::HashMap;
+use std::str::FromStr;
+use std::sync::{Arc, Mutex};
+use xbasic::basic_io::BasicIO;
+use xbasic::expr::ExprValue;
+use xbasic::xbasic::XBasicBuilder;
+
+static EMOJI_MAP: phf::Map<&'static str, &'static str> = phf_map! {
+"cat" => "🐈",
+"chicken" => "🐔",
+"spaghetti" => "🍝",
+"dog" => "🐕",
+"bot" => "🤖",
+"mango" => "🥭",
+"banana" => "🍌",
+"bee" => "🐝"
+};
+
+struct DiscordIO {
+	s: String,
+	frame: Option<FrameBuffer>,
+}
+
+impl DiscordIO {
+	fn new() -> Self {
+		Self {
+			s: String::new(),
+			frame: None,
+		}
+	}
+}
+
+impl BasicIO for DiscordIO {
+	fn read_line(&mut self) -> String {
+		unimplemented!()
+	}
+
+	fn write_line(&mut self, line: String) {
+		self.s += &*(line + "\r\n");
+	}
+}
+
+pub(crate) struct Handler {
+	programs: Arc<Mutex<HashMap<UserId, Program>>>,
+	conn: Arc<Mutex<PgConnection>>,
+}
+
+impl Handler {
+	pub fn new(conn: PgConnection) -> Self {
+		Self {
+			programs: Arc::new(Mutex::new(HashMap::new())),
+			conn: Arc::new(Mutex::new(conn)),
+		}
+	}
+}
+
+#[async_trait]
+impl EventHandler for Handler {
+	async fn message(&self, ctx: Context, msg: Message) {
+		for (key, value) in EMOJI_MAP.entries() {
+			let msg_lower = format!(" {} ", msg.content.to_lowercase());
+			if msg_lower.contains(&format!(" {} ", key))
+				|| msg_lower.contains(&format!(" {}s ", key))
+			{
+				let reaction_type = match ReactionType::from_str(value) {
+					Ok(x) => x,
+					Err(x) => {
+						println!("Could not react: {}", x);
+						return;
+					}
+				};
+				if let Err(e) = msg.react(&ctx, reaction_type).await {
+					println!("Error reacting: {}", e);
+				}
+			}
+		}
+
+		for line in msg.content.split('\n') {
+			if self.programs.lock().unwrap().contains_key(&msg.author.id) {
+				match line {
+					"!STOP" => {
+						self.programs
+							.lock()
+							.unwrap()
+							.remove(&msg.author.id)
+							.unwrap();
+					}
+					"RUN" => {
+						let code = self.programs.lock().unwrap()[&msg.author.id].stringify();
+						let io = DiscordIO::new();
+
+						let (output, fb, errors) = {
+							let mut xbb = XBasicBuilder::new(io);
+							xbb.compute_limit(1000000000);
+
+							xbb.define_function("setframe".to_owned(), 2, |args, io| {
+								let w = args[0].clone().into_decimal() as u32;
+								let h = args[1].clone().into_decimal() as u32;
+
+								io.frame = Some(FrameBuffer::new(w, h));
+
+								ExprValue::Decimal(0.0)
+							})
+							.unwrap();
+
+							xbb.define_function("setpixel".to_owned(), 5, |args, io| {
+								let x = args[0].clone().into_decimal() as u32;
+								let y = args[1].clone().into_decimal() as u32;
+								let red = args[2].clone().into_decimal() as u8;
+								let green = args[3].clone().into_decimal() as u8;
+								let blue = args[4].clone().into_decimal() as u8;
+
+								match &mut io.frame {
+									Some(fb) => {
+										fb.set_pixel(x, y, red, green, blue, 255);
+									}
+									None => {}
+								}
+
+								ExprValue::Decimal(0.0)
+							})
+							.unwrap();
+
+							let mut xb = xbb.build();
+
+							let _ = xb.run(&format!("{}\n", code));
+
+							let errors = if xb.error_handler.had_errors
+								|| xb.error_handler.had_runtime_error
+							{
+								Some(xb.error_handler.errors.join("\n"))
+							} else {
+								None
+							};
+
+							(xb.get_io().s.clone(), xb.get_io().frame.clone(), errors)
+						};
+
+						if !output.is_empty() {
+							msg.channel_id.say(&ctx, output).await.unwrap();
+						}
+
+						if let Some(fb) = &fb {
+							let buf = fb.as_png_vec();
+
+							msg.channel_id
+								.send_message(&ctx, |e| {
+									e.add_file(AttachmentType::Bytes {
+										data: Cow::Borrowed(&buf),
+										filename: "output.png".to_string(),
+									});
+
+									e
+								})
+								.await
+								.unwrap();
+						}
+
+						if let Some(e) = errors {
+							msg.channel_id.say(&ctx, e).await.unwrap();
+						}
+					}
+					"LIST" => {
+						msg.channel_id
+							.say(
+								&ctx,
+								format!(
+									"```\n{}\n```",
+									self.programs.lock().unwrap()[&msg.author.id]
+										.stringy_line_nums()
+								),
+							)
+							.await
+							.unwrap();
+					}
+					_ => {
+						if let Some(name) = line.strip_prefix("SAVE ") {
+							let result = self
+								.programs
+								.lock()
+								.unwrap()
+								.get_mut(&msg.author.id)
+								.unwrap()
+								.save_program(&self.conn.lock().unwrap(), msg.author.id, name);
+							match result {
+								Some(_) => {
+									msg.channel_id
+										.say(&ctx, format!("Saved as {}", name))
+										.await
+										.unwrap();
+								}
+								None => {
+									msg.channel_id
+										.say(&ctx, "Could not save program.")
+										.await
+										.unwrap();
+								}
+							}
+							return;
+						}
+
+						if let Some(name) = line.strip_prefix("LOAD ") {
+							let result = self
+								.programs
+								.lock()
+								.unwrap()
+								.get_mut(&msg.author.id)
+								.unwrap()
+								.load_program(&self.conn.lock().unwrap(), msg.author.id, name);
+							match result {
+								Some(_) => {
+									msg.channel_id
+										.say(&ctx, format!("Loaded {} into memory.", name))
+										.await
+										.unwrap();
+								}
+								None => {
+									msg.channel_id
+										.say(&ctx, "Could not load program into memory.")
+										.await
+										.unwrap();
+								}
+							}
+							return;
+						}
+
+						let mut split = line.splitn(2, ' ');
+						let first = split.next().unwrap();
+						if let Ok(num) = first.parse::<u32>() {
+							match split.next() {
+								Some(x) => {
+									if x.is_empty() {
+										let _ = self
+											.programs
+											.lock()
+											.unwrap()
+											.get_mut(&msg.author.id)
+											.unwrap()
+											.code
+											.remove(&num);
+										return;
+									}
+
+									self.programs
+										.lock()
+										.unwrap()
+										.get_mut(&msg.author.id)
+										.unwrap()
+										.code
+										.insert(num, x.to_owned());
+								}
+								None => {
+									let _ = self
+										.programs
+										.lock()
+										.unwrap()
+										.get_mut(&msg.author.id)
+										.unwrap()
+										.code
+										.remove(&num);
+								}
+							}
+						}
+					}
+				}
+			}
+
+			if line == "!START" {
+				self.programs
+					.lock()
+					.unwrap()
+					.insert(msg.author.id, Program::new());
+			}
+		}
+	}
+
+	async fn ready(&self, _: Context, ready: Ready) {
+		println!("{} is connected", ready.user.name);
+	}
+}
diff --git a/src/main.rs b/src/main.rs
index 6f972db3300db364d54874a505078a06c87226f3..924940418b817ad0f9a67e8265afe6d2dc519e88 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,304 +1,24 @@
-mod config;
+#[macro_use]
+extern crate diesel;
 
-use phf::phf_map;
-use serenity::async_trait;
-use serenity::http::AttachmentType;
-use serenity::model::channel::{Message, ReactionType};
-use serenity::model::id::UserId;
-use serenity::model::prelude::Ready;
-use serenity::prelude::*;
+mod config;
+mod framebuffer;
+mod handler;
+mod models;
+mod program;
+mod schema;
+
+use crate::handler::Handler;
+use diesel::{Connection, PgConnection};
+use dotenv::dotenv;
 use serenity::Client;
-use std::borrow::Cow;
-use std::collections::HashMap;
-use std::str::FromStr;
-use std::sync::Arc;
-use xbasic::basic_io::BasicIO;
-use xbasic::expr::ExprValue;
-use xbasic::xbasic::XBasicBuilder;
-
-#[derive(Clone)]
-struct FrameBuffer {
-	width: u32,
-	height: u32,
-	buffer: Vec<u8>,
-}
-
-impl FrameBuffer {
-	fn new(width: u32, height: u32) -> Self {
-		Self {
-			width,
-			height,
-			buffer: vec![0; width as usize * height as usize * 4],
-		}
-	}
-
-	fn set_pixel(&mut self, x: u32, y: u32, red: u8, green: u8, blue: u8, alpha: u8) {
-		if x >= self.width || y >= self.height {
-			return;
-		}
-
-		let x = x as usize;
-		let y = y as usize;
-		self.buffer[(x + self.width as usize * y) * 4] = red;
-		self.buffer[(x + self.width as usize * y) * 4 + 1] = green;
-		self.buffer[(x + self.width as usize * y) * 4 + 2] = blue;
-		self.buffer[(x + self.width as usize * y) * 4 + 3] = alpha;
-	}
-
-	fn as_png_vec(&self) -> Vec<u8> {
-		let mut buffer: Vec<u8> = Vec::new();
-
-		{
-			let mut encoder = png::Encoder::new(&mut buffer, self.width, self.height);
-			encoder.set_color(png::ColorType::RGBA);
-			encoder.set_depth(png::BitDepth::Eight);
-			let mut writer = encoder.write_header().unwrap();
-			writer.write_image_data(&self.buffer).unwrap();
-		}
-
-		buffer
-	}
-}
-
-struct DiscordIO {
-	s: String,
-	frame: Option<FrameBuffer>,
-}
-
-impl DiscordIO {
-	fn new() -> Self {
-		Self {
-			s: String::new(),
-			frame: None,
-		}
-	}
-}
-
-impl BasicIO for DiscordIO {
-	fn read_line(&mut self) -> String {
-		unimplemented!()
-	}
-
-	fn write_line(&mut self, line: String) {
-		self.s += &*(line + "\r\n");
-	}
-}
-
-struct Program {
-	code: HashMap<u32, String>,
-}
-
-impl Program {
-	fn new() -> Self {
-		Self {
-			code: HashMap::new(),
-		}
-	}
-
-	fn stringify(&self) -> String {
-		let mut code: Vec<(u32, String)> =
-			self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect();
-		code.sort_by(|a, b| a.0.cmp(&b.0));
-
-		code.into_iter()
-			.map(|a| a.1)
-			.collect::<Vec<String>>()
-			.join("\n")
-	}
-
-	fn stringy_line_nums(&self) -> String {
-		let mut code: Vec<(u32, String)> =
-			self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect();
-		code.sort_by(|a, b| a.0.cmp(&b.0));
-
-		code.into_iter()
-			.map(|a| format!("{}\t{}", a.0, a.1))
-			.collect::<Vec<String>>()
-			.join("\n")
-	}
-}
-
-struct Handler {
-	programs: Arc<Mutex<HashMap<UserId, Program>>>,
-}
-
-static EMOJI_MAP: phf::Map<&'static str, &'static str> = phf_map! {
-"cat" => "🐈",
-"chicken" => "🐔",
-"spaghetti" => "🍝",
-"dog" => "🐕",
-"bot" => "🤖",
-"mango" => "🥭",
-"banana" => "🍌",
-"bee" => "🐝"
-};
-
-#[async_trait]
-impl EventHandler for Handler {
-	async fn message(&self, ctx: Context, msg: Message) {
-		for (key, value) in EMOJI_MAP.entries() {
-			let msg_lower = format!(" {} ", msg.content.to_lowercase());
-			if msg_lower.contains(&format!(" {} ", key))
-				|| msg_lower.contains(&format!(" {}s ", key))
-			{
-				let reaction_type = match ReactionType::from_str(value) {
-					Ok(x) => x,
-					Err(x) => {
-						println!("Could not react: {}", x);
-						return;
-					}
-				};
-				if let Err(e) = msg.react(&ctx, reaction_type).await {
-					println!("Error reacting: {}", e);
-				}
-			}
-		}
-
-		for line in msg.content.split('\n') {
-			if self.programs.lock().await.contains_key(&msg.author.id) {
-				match line {
-					"!STOP" => {
-						self.programs.lock().await.remove(&msg.author.id).unwrap();
-					}
-					"RUN" => {
-						let code = self.programs.lock().await[&msg.author.id].stringify();
-						let io = DiscordIO::new();
-
-						let (output, fb, errors) = {
-							let mut xbb = XBasicBuilder::new(io);
-							xbb.compute_limit(1000000000);
-
-							xbb.define_function("setframe".to_owned(), 2, |args, io| {
-								let w = args[0].clone().into_decimal() as u32;
-								let h = args[1].clone().into_decimal() as u32;
-
-								io.frame = Some(FrameBuffer::new(w, h));
-
-								ExprValue::Decimal(0.0)
-							})
-							.unwrap();
-
-							xbb.define_function("setpixel".to_owned(), 5, |args, io| {
-								let x = args[0].clone().into_decimal() as u32;
-								let y = args[1].clone().into_decimal() as u32;
-								let red = args[2].clone().into_decimal() as u8;
-								let green = args[3].clone().into_decimal() as u8;
-								let blue = args[4].clone().into_decimal() as u8;
+use std::env;
 
-								match &mut io.frame {
-									Some(fb) => {
-										fb.set_pixel(x, y, red, green, blue, 255);
-									}
-									None => {}
-								}
+fn establish_connection() -> PgConnection {
+	dotenv().ok();
 
-								ExprValue::Decimal(0.0)
-							})
-							.unwrap();
-
-							let mut xb = xbb.build();
-
-							let _ = xb.run(&format!("{}\n", code));
-
-							let errors = if xb.error_handler.had_errors
-								|| xb.error_handler.had_runtime_error
-							{
-								Some(xb.error_handler.errors.join("\n"))
-							} else {
-								None
-							};
-
-							(xb.get_io().s.clone(), xb.get_io().frame.clone(), errors)
-						};
-
-						msg.channel_id.say(&ctx, output).await.unwrap();
-
-						if let Some(fb) = &fb {
-							let buf = fb.as_png_vec();
-
-							msg.channel_id
-								.send_message(&ctx, |e| {
-									e.add_file(AttachmentType::Bytes {
-										data: Cow::Borrowed(&buf),
-										filename: "output.png".to_string(),
-									});
-
-									e
-								})
-								.await
-								.unwrap();
-						}
-
-						if let Some(e) = errors {
-							msg.channel_id.say(&ctx, e).await.unwrap();
-						}
-					}
-					"LIST" => {
-						msg.channel_id
-							.say(
-								&ctx,
-								format!(
-									"```\n{}\n```",
-									self.programs.lock().await[&msg.author.id].stringy_line_nums()
-								),
-							)
-							.await
-							.unwrap();
-					}
-					_ => {
-						let mut split = line.splitn(2, ' ');
-						let first = split.next().unwrap();
-						if let Ok(num) = first.parse::<u32>() {
-							match split.next() {
-								Some(x) => {
-									if x.is_empty() {
-										let _ = self
-											.programs
-											.lock()
-											.await
-											.get_mut(&msg.author.id)
-											.unwrap()
-											.code
-											.remove(&num);
-										return;
-									}
-
-									self.programs
-										.lock()
-										.await
-										.get_mut(&msg.author.id)
-										.unwrap()
-										.code
-										.insert(num, x.to_owned());
-								}
-								None => {
-									let _ = self
-										.programs
-										.lock()
-										.await
-										.get_mut(&msg.author.id)
-										.unwrap()
-										.code
-										.remove(&num);
-								}
-							}
-						}
-					}
-				}
-			}
-
-			if line == "!START" {
-				self.programs
-					.lock()
-					.await
-					.insert(msg.author.id, Program::new());
-			}
-		}
-	}
-
-	async fn ready(&self, _: Context, ready: Ready) {
-		println!("{} is connected", ready.user.name);
-	}
+	let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
+	PgConnection::establish(&database_url).expect("Error connecting to database")
 }
 
 #[tokio::main]
@@ -306,10 +26,10 @@ async fn main() {
 	let config = config::get_conf();
 	let token = config.token;
 
+	let conn = establish_connection();
+
 	let mut client = Client::builder(&token)
-		.event_handler(Handler {
-			programs: Arc::new(Mutex::new(HashMap::new())),
-		})
+		.event_handler(Handler::new(conn))
 		.await
 		.expect("Error creating client");
 	if let Err(e) = client.start().await {
diff --git a/src/models.rs b/src/models.rs
new file mode 100644
index 0000000000000000000000000000000000000000..0b7f70519457ae868c5e23eb765b968a0f8b3ca1
--- /dev/null
+++ b/src/models.rs
@@ -0,0 +1,18 @@
+use crate::schema::user_programs;
+use bigdecimal::BigDecimal;
+
+#[derive(Queryable)]
+pub struct UserProgram {
+	pub id: i32,
+	pub discord_user_id: u64,
+	pub name: String,
+	pub code: String,
+}
+
+#[derive(Insertable)]
+#[table_name = "user_programs"]
+pub struct NewUserProgram<'a> {
+	pub discord_user_id: BigDecimal,
+	pub name: &'a str,
+	pub code: &'a str,
+}
diff --git a/src/program.rs b/src/program.rs
new file mode 100644
index 0000000000000000000000000000000000000000..dd4a7959dfa1792604ec1de66649fdd3b9bad699
--- /dev/null
+++ b/src/program.rs
@@ -0,0 +1,123 @@
+use crate::models::NewUserProgram;
+use crate::schema::user_programs::columns;
+use crate::schema::user_programs::columns::discord_user_id;
+use crate::schema::user_programs::dsl::user_programs;
+use bigdecimal::{BigDecimal, FromPrimitive};
+use diesel::{BoolExpressionMethods, ExpressionMethods, PgConnection, QueryDsl, RunQueryDsl};
+use serenity::model::id::UserId;
+use std::collections::HashMap;
+
+pub(crate) struct Program {
+	pub(crate) code: HashMap<u32, String>,
+}
+
+impl Program {
+	pub fn new() -> Self {
+		Self {
+			code: HashMap::new(),
+		}
+	}
+
+	pub fn stringify(&self) -> String {
+		let mut code: Vec<(u32, String)> =
+			self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect();
+		code.sort_by(|a, b| a.0.cmp(&b.0));
+
+		code.into_iter()
+			.map(|a| a.1)
+			.collect::<Vec<String>>()
+			.join("\n")
+	}
+
+	pub fn stringy_line_nums(&self) -> String {
+		let mut code: Vec<(u32, String)> =
+			self.code.iter().map(|(a, b)| (*a, b.to_owned())).collect();
+		code.sort_by(|a, b| a.0.cmp(&b.0));
+
+		code.into_iter()
+			.map(|a| format!("{}\t{}", a.0, a.1))
+			.collect::<Vec<String>>()
+			.join("\n")
+	}
+
+	pub fn save_program(&self, conn: &PgConnection, user_id: UserId, name: &str) -> Option<()> {
+		let code = self.stringy_line_nums();
+		let new_program = NewUserProgram {
+			discord_user_id: BigDecimal::from_u64(*user_id.as_u64()).unwrap(),
+			name,
+			code: code.as_str(),
+		};
+
+		diesel::insert_into(user_programs)
+			.values(&new_program)
+			.on_conflict((discord_user_id, columns::name))
+			.do_update()
+			.set(columns::code.eq(&code))
+			.execute(conn)
+			.ok()?;
+
+		Some(())
+	}
+
+	pub fn load_program(&mut self, conn: &PgConnection, user_id: UserId, name: &str) -> Option<()> {
+		let code: Vec<String> = user_programs
+			.filter(
+				columns::discord_user_id
+					.eq(BigDecimal::from_u64(*user_id.as_u64()).unwrap())
+					.and(columns::name.eq(name)),
+			)
+			.limit(1)
+			.select(columns::code)
+			.get_results(conn)
+			.ok()?;
+
+		if code.is_empty() {
+			return None;
+		}
+
+		let code = &code[0];
+
+		self.parse_string(code)?;
+
+		Some(())
+	}
+
+	fn parse_string(&mut self, code: &str) -> Option<()> {
+		let mut valid = true;
+		let code: HashMap<u32, String> = code
+			.split('\n')
+			.map(|line| {
+				let mut iter = line.splitn(2, &[' ', '\t'][..]);
+				// This unwrap_or_else thing is pretty ugly
+				// Is there a better way?
+				let line_num: u32 = iter
+					.next()
+					.unwrap_or_else(|| {
+						valid = false;
+						"0"
+					})
+					.parse()
+					.unwrap_or_else(|_| {
+						valid = false;
+						0
+					});
+				let line_code = iter
+					.next()
+					.unwrap_or_else(|| {
+						valid = false;
+						""
+					})
+					.to_owned();
+
+				(line_num, line_code)
+			})
+			.collect();
+
+		if valid {
+			self.code = code;
+			Some(())
+		} else {
+			None
+		}
+	}
+}
diff --git a/src/schema.rs b/src/schema.rs
new file mode 100644
index 0000000000000000000000000000000000000000..115cafc6b9e8e554f49404145309ebb6916b5957
--- /dev/null
+++ b/src/schema.rs
@@ -0,0 +1,8 @@
+table! {
+    user_programs (id) {
+        id -> Int4,
+        discord_user_id -> Numeric,
+        name -> Varchar,
+        code -> Text,
+    }
+}