diff --git a/.gitignore b/.gitignore
index d1a1edf06f..dc9aec4925 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,5 @@
 
 # Local cache of Rubocop remote config
 .rubocop-*
+
+.env
\ No newline at end of file
diff --git a/Gemfile b/Gemfile
index b1a320395a..9f67097a88 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,3 +11,18 @@ end
 group :development, :test do
   gem 'rubocop', '1.20'
 end
+
+gem "sinatra", "~> 3.0"
+gem "sinatra-contrib", "~> 3.0"
+gem "webrick", "~> 1.8"
+gem "rack-test", "~> 2.1"
+
+gem "pg", "~> 1.5"
+
+gem "bcrypt", "~> 3.1"
+
+gem "dotenv", "~> 2.8"
+
+gem "sib-api-v3-sdk", "~> 9.1"
+
+gem "rackup", "~> 1.0"
diff --git a/Gemfile.lock b/Gemfile.lock
index 66064703c7..ba0cca760b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,13 +1,34 @@
 GEM
   remote: https://rubygems.org/
   specs:
+    addressable (2.8.4)
+      public_suffix (>= 2.0.2, < 6.0)
     ansi (1.5.0)
     ast (2.4.2)
+    bcrypt (3.1.18)
     diff-lcs (1.4.4)
     docile (1.4.0)
+    dotenv (2.8.1)
+    ethon (0.16.0)
+      ffi (>= 1.15.0)
+    ffi (1.15.5)
+    json (2.6.3)
+    multi_json (1.15.0)
+    mustermann (3.0.0)
+      ruby2_keywords (~> 0.0.1)
     parallel (1.20.1)
     parser (3.0.2.0)
       ast (~> 2.4.1)
+    pg (1.5.3)
+    public_suffix (5.0.1)
+    rack (2.2.7)
+    rack-protection (3.0.6)
+      rack
+    rack-test (2.1.0)
+      rack (>= 1.3)
+    rackup (1.0.0)
+      rack (< 3)
+      webrick
     rainbow (3.0.0)
     regexp_parser (2.1.1)
     rexml (3.2.5)
@@ -36,6 +57,11 @@ GEM
     rubocop-ast (1.11.0)
       parser (>= 3.0.1.1)
     ruby-progressbar (1.11.0)
+    ruby2_keywords (0.0.5)
+    sib-api-v3-sdk (9.1.0)
+      addressable (~> 2.3, >= 2.3.0)
+      json (~> 2.1, >= 2.1.0)
+      typhoeus (~> 1.0, >= 1.0.1)
     simplecov (0.21.2)
       docile (~> 1.1)
       simplecov-html (~> 0.11)
@@ -46,18 +72,43 @@ GEM
       terminal-table
     simplecov-html (0.12.3)
     simplecov_json_formatter (0.1.3)
+    sinatra (3.0.6)
+      mustermann (~> 3.0)
+      rack (~> 2.2, >= 2.2.4)
+      rack-protection (= 3.0.6)
+      tilt (~> 2.0)
+    sinatra-contrib (3.0.6)
+      multi_json
+      mustermann (~> 3.0)
+      rack-protection (= 3.0.6)
+      sinatra (= 3.0.6)
+      tilt (~> 2.0)
     terminal-table (3.0.1)
       unicode-display_width (>= 1.1.1, < 3)
+    tilt (2.1.0)
+    typhoeus (1.4.0)
+      ethon (>= 0.9.0)
     unicode-display_width (2.0.0)
+    webrick (1.8.1)
 
 PLATFORMS
   ruby
+  x86_64-linux
 
 DEPENDENCIES
+  bcrypt (~> 3.1)
+  dotenv (~> 2.8)
+  pg (~> 1.5)
+  rack-test (~> 2.1)
+  rackup (~> 1.0)
   rspec
   rubocop (= 1.20)
+  sib-api-v3-sdk (~> 9.1)
   simplecov
   simplecov-console
+  sinatra (~> 3.0)
+  sinatra-contrib (~> 3.0)
+  webrick (~> 1.8)
 
 RUBY VERSION
    ruby 3.0.2p107
diff --git a/README.md b/README.md
index 465eda879b..6b5b913d76 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,85 @@
 Chitter Challenge
 =================
 
+An implementation of the solo project from the Makers Academy Web Applications module. The deployed app can be found at https://chitter-challenge-ct9w.onrender.com/
+
+Running the app locally
+-----------------------
+```
+$ git clone https://github.com/ev-th/chitter-challenge.git
+$ bundle install
+$ createdb chitter
+$ psql -h 127.0.0.1 chitter < chitter.sql
+$ createdb chitter_test
+$ psql -h 127.0.0.1 chitter_test < chitter.sql
+$ rspec
+$ rackup
+```
+
+App features
+------------
+* A user can sign up for an account. Username and email address must be unique.
+* Users can log in and out using their account.
+* Users can post peeps when logged in.
+* Peeps are listed on the homepage in reverse chronological order.
+* Peeps are displayed with the user's name and username and a timestamp.
+* When a user is tagged in a peep, they will receive a notification email.
+
+Technologies
+------------
+* Languages: Ruby, HTML, CSS, SQL
+* git and GitHub
+* Sinatra
+* PostgreSQL
+* Bcrypt
+* Brevo API
+* Render
+* Excalidraw
+* rack
+* RSpec
+* rubocop
+
+Approaching the challenge
+-------------------------
+This was a big challenge with lots of optional extras, so I wasn't sure what I'd be able to achieve in the time. Therefore, I took the approach to create an MVP with the 'straight up' features first, then iteratively add additional features as time allowed.
+
+To start, I planned the views, then the necessary routes:
+![excalidraw_design](./excalidraw_design.png)
+
+Then I planned the database tables. The database has two tables: peeps and users. Users has a one to many relationship with peeps. I set this up and wrote the seeds.
+
+After that, I wrote the database repositories. The fastest option for me was to write the repositories myself as I'm currently unfamiliar with ORMs. This was something I'd hoped to revisit at the end.
+
+Then I wrote the https routes and sanitised user input for empty inputs and any angle brackets that could be an attempt to inject html by a user. This is a pretty basic attempt as sanitising user input and is another aspect of the project that could be examined further.
+
+In order to finish the 'straight up' version of the challenge, I added functionality to the database repository to check whether a username or email already exists in the users table to ensure there are no duplications when a user signs up. If users sign up with a duplicated email or username, or input invalid data, they are taken to a failure page with a 400 status code. I also used the bcrypt gem to encrypt user passwords.
+
+Once the basic features were complete, I moved to the advanced ones. I implemented log in and log out using sessions built into Sinatra. With this, I updated the homepage to adapt to the session. If the user was logged in they could make a new peep or log out. If the user was not logged in they could sign up or log in. Now that the user can log in, I updated the new peep POST route to use session data for user id instead of having the user input their id manually.
+
+Next, I implemented the email sending feature using Brevo. I copied some boilerplate code from their documentation for this and attempted to unit test it, but it was proving to be very complex and didn't seem to be a good use of time. Therefore this is not covered in the test suite. However when testing it manually, it was working. I put secret data in a .env file and added that to my .gitignore. I also added these to my Render environment variables and checked that the emails were also being sent in the deployed version.
+
+Finally, I added some CSS styling. Currently there is one style sheet used for all the views. I had some fun here! 🐦
+
+What would I do with more time?
+-------------------------------
+Overall I'm happy with what I achieved, but there are a number of things I would like to have done if I had more time:
+
+* I missed the final bonus feature to add replies to peeps.
+* Following on from the previous point, I would have liked the app to have pages for users, where you can see only their peeps, along with comments on the peeps. This is where the one to many relationship between the database tables would have made sense.
+* The failure pages for invalid user inputs are very basic and don't give the user information about why the input was invalid. I would have liked more detail here.
+* Bcrypt was severely slowing my tests down so I turned down the cost slightly to make it more workable while in development. I would like to work out how to only turn down costs in tests and leave it higher for deployed code.
+* HTML form input sanitisation was very basic and could have been developed further.
+* I would implement ActiveRecord as an ORM.
+* Overall, I'm happy with my testing and used TDD throughout the majority of the process. However, I struggled to use testing when implementing the email sender, so this is not covered by the test suite.
+* I'd like to have spent some more time on styling.
+
+---
+
+The following are the specifications for the project provided by Makers:
+
+Chitter Challenge Specifications
+================================
+
 * Feel free to use Google, your notes, books, etc. but work on your own
 * If you refer to the solution of another coach or student, please put a link to that in your README
 * If you have a partial solution, **still check in a partial solution**
diff --git a/app.rb b/app.rb
new file mode 100644
index 0000000000..7f2c645170
--- /dev/null
+++ b/app.rb
@@ -0,0 +1,152 @@
+require 'bcrypt'
+require 'sinatra/base'
+require 'sinatra/reloader'
+require_relative 'lib/peep_repository'
+require_relative 'lib/user_repository'
+require_relative 'lib/database_connection'
+require_relative 'lib/email_sender'
+
+DatabaseConnection.connect
+
+class Application < Sinatra::Base
+  enable :sessions
+
+  configure :development do
+    register Sinatra::Reloader
+    also_reload 'lib/user_repository'
+    also_reload 'lib/peep_repository'
+  end
+
+  get '/' do
+    peep_repo = PeepRepository.new
+    peeps = peep_repo.all
+    @peeps = peeps.sort_by(&:time_posted).reverse
+
+    user_repo = UserRepository.new
+    @peeps.each { |peep| peep.user = user_repo.find(peep.user_id) }
+    @user = session[:user_id].nil? ? nil : user_repo.find(session[:user_id])
+
+    erb(:index)
+  end
+
+  get '/login' do
+    erb(:login)
+  end
+
+  get '/sign-up' do
+    erb(:sign_up)
+  end
+
+  get '/new-peep' do
+    return redirect('/login') if session[:user_id].nil?
+    erb(:new_peep)
+  end
+  
+  get '/logout' do
+    session[:user_id] = nil
+    return redirect('/')
+  end
+
+  post '/login' do
+    repo = UserRepository.new
+    email = params[:email]
+    password = params[:password]
+
+    unless input_valid?(email) && input_valid?(password) && repo.email_exists?(email)
+      status 400
+      return erb(:login_failure)
+    end
+
+    unless repo.correct_password?(email, password)
+      status 400
+      return erb(:login_failure)
+    end
+
+    @user = repo.find_by_email(email)
+    session[:user_id] = @user.id
+    return erb(:login_success)
+  end
+  
+  post '/sign-up' do
+    unless params.values.all? { |input| input_valid?(input) }
+      status 400
+      return erb(:sign_up_failure)
+    end
+    
+    repo = UserRepository.new
+
+    if repo.email_exists?(params[:email]) || repo.username_exists?(params[:username])
+      status 400
+      return erb(:sign_up_failure) 
+    end
+
+    user = get_user_from_params(params)
+    repo.create(user)
+    return erb(:sign_up_success)
+  end
+
+  post '/new-peep' do
+    unless params.values.all? { |input| input_valid?(input) }
+      status 400
+      return erb(:new_peep_failure)
+    end
+
+    repo = PeepRepository.new
+    peep = get_peep_from_params(params)
+    repo.create(peep)
+
+    email_tagged_users(peep)
+    erb(:new_peep_success)
+  end
+
+  private
+
+  def get_user_from_params(params)
+    user = User.new
+    user.email = params[:email]
+    user.password = params[:password]
+    user.name = params[:name]
+    user.username = params[:username]
+    user
+  end
+
+  def get_peep_from_params(params)
+    peep = Peep.new
+    peep.content = params[:content]
+    peep.time_posted = params[:time_posted] || Time.new
+    peep.user_id = session[:user_id]
+    peep
+  end
+
+  def email_tagged_users(peep)
+    users = get_tagged_users(peep)
+    return if users.nil?
+    
+    users.each do |user|
+      email_sender = EmailSender.new(
+        user.email,
+        '<html>You have been tagged in a peep!</html>'
+      )
+      email_sender.send_email
+    end
+  end
+  
+  def get_tagged_users(peep)
+    words = peep.content.split
+    tags = words.select { |word| word.start_with?("@") }
+    usernames = tags.map { |tag| tag[1..] }
+
+    repo = UserRepository.new
+
+    users = []
+    usernames.each do |username|
+      user = repo.find_by_username(username)
+      users << user unless user.nil?
+    end
+    users
+  end
+
+  def input_valid?(input)
+    input != '' && !input.match(/[<>]/)
+  end
+end
diff --git a/chitter.sql b/chitter.sql
new file mode 100644
index 0000000000..77b9dce286
--- /dev/null
+++ b/chitter.sql
@@ -0,0 +1,19 @@
+DROP TABLE IF EXISTS users, peeps;
+
+CREATE TABLE users (
+  id SERIAL PRIMARY KEY,
+  email text,
+  password text,
+  name text,
+  username text
+);
+
+CREATE TABLE peeps (
+  id SERIAL PRIMARY KEY,
+  content text,
+  time_posted timestamp,
+  user_id integer,
+  constraint fk_user foreign key(user_id)
+    references users(id)
+    on delete cascade
+);
\ No newline at end of file
diff --git a/config.ru b/config.ru
new file mode 100644
index 0000000000..af14ef717e
--- /dev/null
+++ b/config.ru
@@ -0,0 +1,2 @@
+require './app'
+run Application
diff --git a/excalidraw_design.png b/excalidraw_design.png
new file mode 100644
index 0000000000..e5b6bfb4f0
Binary files /dev/null and b/excalidraw_design.png differ
diff --git a/lib/database_connection.rb b/lib/database_connection.rb
new file mode 100644
index 0000000000..34b41dff47
--- /dev/null
+++ b/lib/database_connection.rb
@@ -0,0 +1,22 @@
+require 'pg'
+
+class DatabaseConnection
+  def self.connect
+    if ENV['DATABASE_URL'] != nil
+      @connection = PG.connect(ENV['DATABASE_URL'])
+      return
+    end
+
+    database_name = ENV['ENV'] == 'test' ? 'chitter_test' : 'chitter'
+    @connection = PG.connect({ host: '127.0.0.1', dbname: database_name })
+  end
+
+  def self.exec_params(query, params)
+    if @connection.nil?
+      raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\
+      'the database was never opened. Did you make sure to call first the method '\
+      '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?'
+    end
+    @connection.exec_params(query, params)
+  end
+end
diff --git a/lib/email_sender.rb b/lib/email_sender.rb
new file mode 100644
index 0000000000..c1adf79612
--- /dev/null
+++ b/lib/email_sender.rb
@@ -0,0 +1,41 @@
+require 'dotenv'
+require 'sib-api-v3-sdk'
+Dotenv.load
+
+class EmailSender
+  def initialize(recipient, html_message, email_api = SibApiV3Sdk)
+    @email_api = email_api
+    @recipient = recipient
+    @html_message = html_message
+
+    @email_api.configure do |config|
+      config.api_key['api-key'] = ENV['BREVO_API_KEY']
+      config.api_key['partner-key'] = ENV['BREVO_API_KEY']
+    end
+  end
+
+  def send_email
+
+    api_instance = @email_api::TransactionalEmailsApi.new
+
+    send_smtp_email = @email_api::SendSmtpEmail.new
+    send_smtp_email.html_content = @html_message
+    send_smtp_email.subject = "You've been tagged!"
+    
+    email_to = @email_api::SendSmtpEmailTo.new
+    email_to.email = @recipient
+    send_smtp_email.to = [email_to]
+    
+    email_from = @email_api::SendSmtpEmailSender.new
+    email_from.email = ENV['EMAIL_FROM']
+    send_smtp_email.sender = email_from
+    
+    begin
+      result = api_instance.send_transac_email(send_smtp_email)
+      p result
+    rescue @email_api::ApiError => e
+      binding.irb
+      puts "Exception when calling TransactionalEmailsApi->send_transac_email: #{e}"
+    end
+  end
+end
diff --git a/lib/peep.rb b/lib/peep.rb
new file mode 100644
index 0000000000..dc11962c5b
--- /dev/null
+++ b/lib/peep.rb
@@ -0,0 +1,8 @@
+class Peep
+  attr_accessor :id, :content, :time_posted, :user_id, :user
+
+  def formatted_time
+    return nil if @time_posted.nil?
+    @time_posted.strftime('%d %b %Y at %H:%M')
+  end
+end
diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb
new file mode 100644
index 0000000000..a42b3e99bc
--- /dev/null
+++ b/lib/peep_repository.rb
@@ -0,0 +1,40 @@
+require_relative './peep'
+
+class PeepRepository
+  def all
+    sql = 'SELECT * FROM peeps;'
+    records = DatabaseConnection.exec_params(sql, [])
+    records.map { |record| create_peep_from_record(record) }
+  end
+
+  def find(id)
+    sql = 'SELECT * FROM peeps WHERE id = $1;'
+    records = DatabaseConnection.exec_params(sql, [id])
+    return create_peep_from_record(records.first)
+  end
+
+  def create(peep)
+    sql = 'INSERT INTO peeps (content, time_posted, user_id)
+             VALUES ($1, $2, $3)'
+    params = [peep.content, peep.time_posted, peep.user_id]
+    DatabaseConnection.exec_params(sql, params)
+  end
+
+  private
+
+  def create_peep_from_record(record)
+    peep = Peep.new
+    peep.id = record['id'].to_i
+    peep.content = record['content']
+    peep.time_posted = timestamp_to_time_object(record['time_posted'])
+    peep.user_id = record['user_id'].to_i
+    return peep
+  end
+
+  def timestamp_to_time_object(timestamp)
+    date, time = timestamp.split
+    year, month, day = date.split("-")
+    hour, minute, second = time.split(":")
+    return Time.new(year, month, day, hour, minute, second)
+  end
+end
diff --git a/lib/user.rb b/lib/user.rb
new file mode 100644
index 0000000000..1f12d2cc2e
--- /dev/null
+++ b/lib/user.rb
@@ -0,0 +1,3 @@
+class User
+  attr_accessor :id, :email, :password, :name, :username
+end
diff --git a/lib/user_repository.rb b/lib/user_repository.rb
new file mode 100644
index 0000000000..4fb59b7e84
--- /dev/null
+++ b/lib/user_repository.rb
@@ -0,0 +1,67 @@
+require 'bcrypt'
+require_relative './user'
+
+class UserRepository
+  def all
+    sql = 'SELECT * FROM users;'
+    records = DatabaseConnection.exec_params(sql, [])
+    records.map { |record| create_user_from_record(record) }
+  end
+
+  def find(id)
+    sql = 'SELECT * FROM users WHERE id = $1'
+    records = DatabaseConnection.exec_params(sql, [id])
+    records.ntuples.zero? ? nil : create_user_from_record(records.first)
+  end
+
+  def find_by_email(email)
+    sql = 'SELECT * FROM users WHERE email = $1'
+    records = DatabaseConnection.exec_params(sql, [email])
+    records.ntuples.zero? ? nil : create_user_from_record(records.first)
+  end
+
+  def find_by_username(username)
+    sql = 'SELECT * FROM users WHERE username = $1'
+    records = DatabaseConnection.exec_params(sql, [username])
+    records.ntuples.zero? ? nil : create_user_from_record(records.first)
+  end
+
+  def create(user)
+    encrypted_password = BCrypt::Password.create(user.password, cost: 7)
+
+    sql = 'INSERT INTO users (email, password, name, username)
+             VALUES ($1, $2, $3, $4)'
+    params = [user.email, encrypted_password, user.name, user.username]
+    DatabaseConnection.exec_params(sql, params)
+  end
+
+  def username_exists?(username)
+    sql = 'SELECT username FROM users WHERE username = $1'
+    records = DatabaseConnection.exec_params(sql, [username])
+    records.ntuples.positive?
+  end
+  
+  def email_exists?(email)
+    sql = 'SELECT email FROM users WHERE email = $1'
+    records = DatabaseConnection.exec_params(sql, [email])
+    records.ntuples.positive?
+  end
+
+  def correct_password?(email, password)
+    user = find_by_email(email)
+    stored_password = BCrypt::Password.new(user.password)
+    stored_password == password
+  end
+
+  private
+
+  def create_user_from_record(record)
+    user = User.new
+    user.id = record['id'].to_i
+    user.email = record['email']
+    user.password = record['password']
+    user.name = record['name']
+    user.username = record['username']
+    return user
+  end
+end
diff --git a/public/pigeons.jpeg b/public/pigeons.jpeg
new file mode 100644
index 0000000000..bf228bf6dd
Binary files /dev/null and b/public/pigeons.jpeg differ
diff --git a/public/styles.css b/public/styles.css
new file mode 100644
index 0000000000..2cb6b43e6b
--- /dev/null
+++ b/public/styles.css
@@ -0,0 +1,52 @@
+* {
+    font-family: "Comic Sans MS", "Comic Sans", cursive;
+    text-align: center;
+    font-weight: bold;
+    color: deeppink;
+}
+
+body {
+    background-image: url(pigeons.jpeg);
+}
+
+h1 {
+    font-size: 92;
+    text-shadow: 2px 2px black;
+}
+
+h2 {
+    font-size: 60;
+    text-shadow: 2px 2px black;
+}
+
+p {
+    font-size: 30px;
+    text-shadow: 1px 1px black;
+}
+
+label {
+    text-shadow: 1px 1px black;
+}
+
+.peep-container {
+    width: 45%;
+    margin: 10px auto 10px auto;
+    border: 5px solid black;
+    border-radius: 10px;
+    background-color: rgba(255,255,255,0.7);
+    background-position: center;
+    background-repeat: no-repeat;
+    background-size: cover;
+}
+
+#content {
+    font-size: 40px;
+}
+
+#time {
+    font-size: 20px;
+}
+
+#names {
+    font-size: 20px;
+}
\ No newline at end of file
diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb
new file mode 100644
index 0000000000..7aff94ee9a
--- /dev/null
+++ b/spec/integration/app_spec.rb
@@ -0,0 +1,649 @@
+require 'spec_helper'
+require 'rack/test'
+require_relative '../../app'
+
+def log_in_a_user
+  user = User.new
+  user.email = 'hello@gmail.com'
+  user.password = 'new_pass_123!'
+  user.name = 'My Name'
+  user.username = 'new_username'
+  
+  repo = UserRepository.new
+  repo.create(user)
+  
+  post('/login', email: 'hello@gmail.com', password: 'new_pass_123!')
+end
+
+describe Application do
+  include Rack::Test::Methods
+
+  let(:app) { Application.new }
+
+  before(:each) do
+    reset_users_table
+    reset_peeps_table
+  end
+
+  describe 'GET /' do
+    it 'returns 200 OK' do
+      response = get('/')
+      expect(response.status).to eq 200
+    end
+
+    it 'returns returns html with all peeps' do
+      response = get('/')
+
+      expect(response.body).to include '<h2>All Peeps</h2>'
+      
+      expect(response.body).to include 'content_1'
+      expect(response.body).to include '1 May 2023 at 17:15'
+      expect(response.body).to include 'name_1'
+      expect(response.body).to include 'username_1'
+      
+      expect(response.body).to include 'content_3'
+      expect(response.body).to include '21 Jun 2022 at 00:01'
+      expect(response.body).to include 'name_3'
+      expect(response.body).to include 'username_3'
+      
+      expect(response.body).to include 'content_4'
+      expect(response.body).to include '21 Jun 2022 at 22:01'
+      expect(response.body).to include 'name_4'
+      expect(response.body).to include 'username_4'
+    end
+
+    it 'returns html with link to sign up and login when not logged in' do
+      response = get('/')
+      expect(response.body).to include '<a href="/sign-up">Sign Up</a>'
+      expect(response.body).to include '<a href="/login">Log In</a>'
+    end
+    
+    it 'does not return html with link to log in or sign up when logged in' do
+      log_in_a_user
+      response = get('/')
+      expect(response.body).not_to include '<a href="/login">Log In</a>'
+      expect(response.body).not_to include '<a href="/sign-up">Sign Up</a>'
+    end
+    
+    it 'returns html with link to post a new peep when logged in' do
+      log_in_a_user
+      response = get('/')
+      expect(response.body).to include '<a href="/new-peep">Add new peep</a>'
+    end
+    
+    it 'returns no link to make a new peep when not logged in' do
+      response = get('/')
+      expect(response.body).not_to include '<a href="/new-peep">Add new peep</a>'
+    end
+    
+    it 'returns html with link to log out when logged in' do
+      log_in_a_user
+      response = get('/')
+      expect(response.body).to include '<a href="/logout">Log Out</a>'
+    end
+
+    it 'does not return html with link to log out when not logged in' do
+      response = get('/')
+      expect(response.body).not_to include '<a href="/logout">Log Out</a>'
+    end
+  end
+
+  describe 'GET /login' do
+    it "returns 200 OK" do
+      response = get('/login')
+      expect(response.status).to eq 200
+    end
+
+    it 'returns html with login form using POST /login route' do
+      response = get('login')
+      expect(response.body).to include '<form action="/login" method="POST">'
+      expect(response.body).to include '<input type="text" name="email">'
+      expect(response.body).to include '<input type="text" name="password">'
+      expect(response.body).to include '<input type="submit">'
+    end
+
+    it 'returns html with link back to homepage' do
+      response = get('/login')
+      expect(response.body).to include '<a href="/">Back to homepage</a>'
+    end
+  end
+
+  describe 'GET /logout' do
+    it 'logs the user out' do
+      log_in_a_user
+      response = get('/logout')
+      expect(response.status).to eq 302
+    end
+  end
+  
+  describe 'GET /sign-up' do
+    it 'returns 200 OK' do
+      response = get('/sign-up')
+      expect(response.status).to eq 200
+    end
+    
+    it 'returns html with sign up form using POST /sign-up route' do
+      response = get('/sign-up')
+      expect(response.body).to include '<form action="/sign-up" method="POST">'
+      expect(response.body).to include '<input type="text" name="email">'
+      expect(response.body).to include '<input type="text" name="password">'
+      expect(response.body).to include '<input type="text" name="name">'
+      expect(response.body).to include '<input type="text" name="username">'
+      expect(response.body).to include '<input type="submit">'
+    end
+    
+    it 'returns html with link back to homepage' do
+      response = get('/sign-up')
+      expect(response.body).to include '<a href="/">Back to homepage</a>'
+    end
+  end
+  
+  describe 'GET /new-peep' do
+    context 'when user is logged in' do
+      it 'returns 200 OK' do
+        log_in_a_user
+        response = get('/new-peep')
+        expect(response.status).to eq 200
+      end
+      
+      it 'returns html with new peep form using POST /new-peep route' do
+        log_in_a_user
+        response = get('/new-peep')
+        expect(response.body).to include '<form action="/new-peep" method="POST">'
+        expect(response.body).to include '<input type="textarea" name="content">'
+        expect(response.body).to include '<input type="submit">'
+      end
+      
+      it 'returns html with link back to the homepage' do
+        log_in_a_user
+        response = get('/new-peep')
+        expect(response.body).to include '<a href="/">Back to homepage</a>'
+      end
+    end
+    
+    context 'when user is not logged in' do
+      it 'redirects to the login page' do
+        response = get('/new-peep')
+        expect(response.status).to eq 302
+      end
+    end
+  end
+
+  describe 'POST /login' do
+    context 'when used with valid params' do
+      it 'returns 200 OK' do
+        response = log_in_a_user
+        expect(response.status).to eq 200
+      end
+
+      it 'returns html success message' do
+        response = log_in_a_user
+        expect(response.body).to include '<h1>Success!</h1>'
+      end
+      
+      it 'returns html with link back to the homepage' do
+        response = log_in_a_user
+        expect(response.body).to include '<p><a href="/">Back to homepage</a></p>'
+      end
+      
+      it 'logs the user in' do
+        response = log_in_a_user
+        expect(response.body).to include '<h2>Hello My Name! You are logged in as new_username</h2>'
+      end
+    end
+    
+    context 'when used with invalid params' do
+      context 'when email is blank' do
+        it 'returns 400 Bad Request and failure page' do
+          response = post('/login', email: '', password: 'new_pass_123!')
+          expect(response.status).to eq 400
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<h2>One or more of your inputs was invalid</h2>'
+          expect(response.body).to include '<a href="/login">Try again</a>'
+        end
+      end
+      
+      context 'when email does not exist in the database' do
+        it 'returns 400 Bad Request and failure page' do
+          response = post('/login', email: 'fake_email@email.com', password: 'new_pass_123!')
+          expect(response.status).to eq 400
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<h2>One or more of your inputs was invalid</h2>'
+          expect(response.body).to include '<a href="/login">Try again</a>'
+        end
+      end
+      
+      context 'when password is incorrect' do
+        it 'returns 400 Bad Request and failure page' do
+          user = User.new
+          user.email = 'hello@gmail.com'
+          user.password = 'new_pass_123!'
+          user.name = 'My Name'
+          user.username = 'new_username'
+  
+          repo = UserRepository.new
+          repo.create(user)
+          
+          response = post('/login', email: 'hello@gmail.com', password: 'bad_pass')
+          expect(response.status).to eq 400
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<h2>One or more of your inputs was invalid</h2>'
+          expect(response.body).to include '<a href="/login">Try again</a>'
+        end
+      end
+    end
+  end
+  
+  describe 'POST /sign-up' do
+    context 'when used with valid params' do
+      it 'returns 200 OK' do
+        response = post(
+          '/sign-up',
+          email: 'new@gmail.com',
+          password: 'new_password',
+          name: 'New Name',
+          username: 'new_username'
+        )
+        expect(response.status).to eq 200
+      end
+      
+      it 'returns html with success message' do
+        response = post(
+          '/sign-up',
+          email: 'new@gmail.com',
+          password: 'new_password',
+          name: 'New Name',
+          username: 'new_username'
+        )
+        expect(response.body).to include '<h1>Success!</h1>'
+        expect(response.body).to include '<h2>Thanks for signing up!</h2>'
+      end
+      
+      it 'returns html with link back to the homepage' do
+        response = post('/sign-up')
+        expect(response.body).to include '<a href="/">Back to homepage</a>'
+      end
+      
+      it 'adds a new user to the database' do
+        response = post(
+          '/sign-up',
+          email: 'new@gmail.com',
+          password: 'new_password',
+          name: 'New Name',
+          username: 'new_username'
+        )
+        
+        repo = UserRepository.new
+        user = repo.find(5)
+
+        expect(user.email).to eq 'new@gmail.com'
+        expect(user.name).to eq 'New Name'
+        expect(user.username).to eq 'new_username'
+
+        stored_password = BCrypt::Password.new(user.password)
+        expect(stored_password).to eq 'new_password'
+      end
+    end
+    
+    context 'when used with invalid params' do
+      context 'when email already exists' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'email_1',
+            password: 'new_password',
+            name: 'New Name',
+            username: 'new_username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end        
+      end
+
+      context 'when username already exists' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: 'new_password',
+            name: 'New Name',
+            username: 'username_1'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end        
+      end
+
+      context 'when email is empty' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: '',
+            password: 'new_password',
+            name: 'New Name',
+            username: 'new_username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when password is empty' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: '',
+            name: 'New Name',
+            username: 'new_username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when name is empty' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: 'new_password',
+            name: '',
+            username: 'new_username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when username is empty' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: 'new_password',
+            name: 'New Name',
+            username: ''
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when email has invalid characters' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email<',
+            password: 'new_password',
+            name: 'New Name',
+            username: 'username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when password has invalid characters' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: 'new_password>',
+            name: 'New Name',
+            username: 'username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when name has invalid characters' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: 'new_password',
+            name: 'New Name<html>',
+            username: 'username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      context 'when username has invalid characters' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/sign-up',
+            email: 'new_email',
+            password: 'new_password',
+            name: 'New Name',
+            username: '<script>username'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/sign-up">Try again</a>'
+        end
+      end
+
+      it 'doesn\'t update the database' do
+        repo = UserRepository.new
+        number_of_records = repo.all.length
+
+        response = post(
+          '/sign-up',
+          email: 'new_email',
+          password: 'new_password',
+          name: 'New Name',
+          username: '<script>username'
+        )
+
+        updated_number_of_records = repo.all.length
+        expect(updated_number_of_records).to eq number_of_records
+      end
+    end
+  end
+
+  describe 'POST /new-peep' do
+    context 'when used with valid params' do
+      it 'returns 200 OK' do
+        response = post(
+          '/new-peep',
+          content: 'test_content',
+          time_posted: Time.now,
+          user_id: '4'
+        )
+        expect(response.status).to eq 200
+      end
+      
+      it 'returns html with success message' do
+        response = post(
+          '/new-peep',
+          content: 'test_content',
+          time_posted: Time.now,
+          user_id: '4'
+        )
+        expect(response.body).to include '<h1>Success!</h1>'
+        expect(response.body).to include '<h2>You added a new peep!</h2>'
+      end
+      
+      it 'returns html with link back to the homepage' do
+        response = post(
+          '/new-peep',
+          content: 'test_content',
+          time_posted: Time.now,
+          user_id: '4'
+        )
+        expect(response.body).to include '<a href="/">Back to homepage</a>'
+      end
+      
+      it 'returns html with option to create another new peep' do
+        response = post(
+          '/new-peep',
+          content: 'test_content',
+          time_posted: Time.now,
+          user_id: '4'
+        )
+        expect(response.body).to include '<a href="/new-peep">Add another peep</a>'
+      end
+      
+      it 'adds a new peep to the database' do
+        log_in_a_user
+
+        response = post(
+          '/new-peep',
+          content: 'this is new content',
+          time_posted: Time.new(2000, 1, 2, 3, 4, 5)
+        )
+        repo = PeepRepository.new
+        expect(repo.all).to include(
+          have_attributes(
+            id: 8,
+            content: 'this is new content',
+            time_posted: Time.new(2000, 1, 2, 3, 4, 5),
+            user_id: 5
+          )
+        )
+      end
+    end
+
+    context 'when used with invalid params' do
+      context 'when content is empty' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/new-peep',
+            content: '',
+            time_posted: Time.now,
+            user_id: '4'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/new-peep">Try again</a>'
+        end
+      end
+
+      context 'when user_id is empty' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/new-peep',
+            content: 'some new content',
+            time_posted: Time.now,
+            user_id: ''
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/new-peep">Try again</a>'
+        end
+      end
+
+      context 'when content has invalid characters' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/new-peep',
+            content: 'some new content<html>',
+            time_posted: Time.now,
+            user_id: '3'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/new-peep">Try again</a>'
+        end
+      end
+
+      context 'when user_id has invalid characters' do
+        it 'returns 400 Bad Request and html failure page' do
+          response = post(
+            '/new-peep',
+            content: 'some new content',
+            time_posted: Time.now,
+            user_id: '3<script>'
+          )
+          expect(response.status).to eq 400
+          expect(response.body).to include '<h1>Error!</h1>'
+          expect(response.body).to include (
+            '<h2>One or more of your inputs was invalid</h2>'
+          )
+          expect(response.body).to include '<a href="/">Back to homepage</a>'
+          expect(response.body).to include '<a href="/new-peep">Try again</a>'
+        end
+      end
+
+      it 'doesn\'t update the database' do
+        repo = PeepRepository.new
+        number_of_records = repo.all.length
+
+        response = post(
+          '/new-peep',
+          content: 'some new content',
+          time_posted: Time.now,
+          user_id: '3<script>'
+        )
+
+        updated_number_of_records = repo.all.length
+        expect(updated_number_of_records).to eq number_of_records
+      end
+    end
+  end
+end
diff --git a/spec/peep_repository_spec.rb b/spec/peep_repository_spec.rb
new file mode 100644
index 0000000000..6dd3887829
--- /dev/null
+++ b/spec/peep_repository_spec.rb
@@ -0,0 +1,76 @@
+require 'peep_repository'
+
+describe PeepRepository do
+  before(:each) do
+    reset_peeps_table
+  end
+
+  describe '#all' do
+    it 'returns a list of all peeps' do
+      repo = PeepRepository.new
+      peeps = repo.all
+      expect(peeps.length).to eq 7
+
+      expect(peeps).to include(
+        have_attributes(
+          id: 1,
+          content: 'content_1',
+          time_posted: Time.new(2023, 5, 1, 17, 15, 32),
+          user_id: 1
+        )
+      )
+
+      expect(peeps).to include(
+        have_attributes(
+          id: 5,
+          content: 'content_5',
+          time_posted: Time.new(1999, 4, 19, 16, 59, 59),
+          user_id: 1
+        )
+      )
+    end
+
+    describe '#find' do
+      it 'returns a peep by id' do
+        repo = PeepRepository.new
+        peep = repo.find(1)
+
+        expect(peep.id).to eq 1
+        expect(peep.content).to eq  'content_1'
+        expect(peep.time_posted).to eq Time.new(2023, 5, 1, 17, 15, 32)
+        expect(peep.user_id).to eq 1
+      end
+
+      it 'returns another peep by id' do
+        repo = PeepRepository.new
+        peep = repo.find(4)
+
+        expect(peep.id).to eq 4
+        expect(peep.content).to eq  'content_4'
+        expect(peep.time_posted).to eq Time.new(2022, 6, 21, 22, 1, 2)
+        expect(peep.user_id).to eq 4
+      end
+    end
+
+    describe '#create' do
+      it 'adds a new peep to the database' do
+        peep = Peep.new
+        peep.content = 'new_content'
+        peep.time_posted = Time.new('2023-05-05 17:53:00')
+        peep.user_id = 3
+
+        repo = PeepRepository.new
+        repo.create(peep)
+
+        expect(repo.all).to include(
+          have_attributes(
+            id: 8,
+            content: 'new_content',
+            time_posted: Time.new('2023-05-05 17:53:00'),
+            user_id: 3
+          )
+        )
+      end
+    end
+  end
+end
diff --git a/spec/peep_spec.rb b/spec/peep_spec.rb
new file mode 100644
index 0000000000..416647cf50
--- /dev/null
+++ b/spec/peep_spec.rb
@@ -0,0 +1,16 @@
+require 'peep'
+
+RSpec.describe Peep do
+  it 'formats the date' do
+    peep = Peep.new
+    peep.time_posted = Time.new(2023, 5, 11, 13, 27, 0)
+    expect(peep.formatted_time).to eq '11 May 2023 at 13:27'
+  end
+
+  context 'there is no time_posted' do
+    it '#formatted_time returns nil' do
+      peep = Peep.new
+      expect(peep.formatted_time).to eq nil
+    end
+  end
+end
diff --git a/spec/seeds_peeps.sql b/spec/seeds_peeps.sql
new file mode 100644
index 0000000000..f4ef188aa5
--- /dev/null
+++ b/spec/seeds_peeps.sql
@@ -0,0 +1,13 @@
+TRUNCATE TABLE peeps RESTART IDENTITY;
+
+INSERT INTO peeps
+  (content, time_posted, user_id)
+  VALUES
+    ('content_1', '2023-05-01 17:15:32', 1),
+    ('content_2', '2022-04-21 04:03:02', 2),
+    ('content_3', '2022-06-21 00:01:02', 3),
+    ('content_4', '2022-06-21 22:01:02', 4),
+    ('content_5', '1999-04-19 16:59:59', 1),
+    ('content_6', '2005-12-27 03:45:06', 1),
+    ('content_7', '2007-08-13 17:17:17', 4)
+;
\ No newline at end of file
diff --git a/spec/seeds_users.sql b/spec/seeds_users.sql
new file mode 100644
index 0000000000..89f07366aa
--- /dev/null
+++ b/spec/seeds_users.sql
@@ -0,0 +1,10 @@
+TRUNCATE TABLE users RESTART IDENTITY CASCADE;
+
+INSERT INTO users
+  (email, password, name, username)
+  VALUES
+    ('email_1', 'password_1', 'name_1', 'username_1'),
+    ('email_2', 'password_2', 'name_2', 'username_2'),
+    ('email_3', 'password_3', 'name_3', 'username_3'),
+    ('email_4', 'password_4', 'name_4', 'username_4')
+;
\ No newline at end of file
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 252747d899..d22d532734 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,5 +1,6 @@
 require 'simplecov'
 require 'simplecov-console'
+require 'database_connection'
 
 SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
   SimpleCov::Formatter::Console,
@@ -15,3 +16,18 @@
     puts "\e[33mTry it now! Just run: rubocop\e[0m"
   end
 end
+
+ENV['ENV'] = 'test'
+DatabaseConnection.connect
+
+def reset_peeps_table
+  sql_seeds = File.read('spec/seeds_peeps.sql')
+  connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' })
+  connection.exec(sql_seeds)
+end
+
+def reset_users_table
+  sql_seeds = File.read("spec/seeds_users.sql")
+  connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' })
+  connection.exec sql_seeds
+end
diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb
new file mode 100644
index 0000000000..b68a34c037
--- /dev/null
+++ b/spec/user_repository_spec.rb
@@ -0,0 +1,217 @@
+require 'user_repository'
+
+describe UserRepository do
+  before(:each) do
+    reset_users_table
+  end
+
+  describe '#all' do
+    it 'returns a list of all users' do
+      repo = UserRepository.new
+      users = repo.all
+
+      expect(users.length).to eq 4
+
+      expect(users).to include(
+        have_attributes(
+          id: 1,
+          email: 'email_1',
+          password: 'password_1',
+          name: 'name_1',
+          username: 'username_1'
+        )
+      )
+
+      expect(users).to include(
+        have_attributes(
+          id: 3,
+          email: 'email_3',
+          password: 'password_3',
+          name: 'name_3',
+          username: 'username_3'
+        )
+      )
+    end
+  end
+
+  describe '#find' do
+    it 'returns a user by id' do
+      repo = UserRepository.new
+      user = repo.find(1)
+      expect(user.id).to eq 1
+      expect(user.email).to eq 'email_1'
+      expect(user.password).to eq 'password_1'
+      expect(user.name).to eq 'name_1'
+      expect(user.username).to eq 'username_1'
+    end
+    
+    it 'returns another user by id' do
+      repo = UserRepository.new
+      user = repo.find(3)
+      expect(user.id).to eq 3
+      expect(user.email).to eq 'email_3'
+      expect(user.password).to eq 'password_3'
+      expect(user.name).to eq 'name_3'
+      expect(user.username).to eq 'username_3'
+    end
+
+    it 'returns nil if id does not exist' do
+      repo = UserRepository.new
+      user = repo.find(23)
+      expect(user).to eq nil
+    end
+  end
+  
+  describe '#find_by_email' do
+    it 'returns a user by email' do
+      repo = UserRepository.new
+      user = repo.find_by_email('email_1')
+      
+      expect(user.id).to eq 1
+      expect(user.email).to eq 'email_1'
+      expect(user.password).to eq 'password_1'
+      expect(user.name).to eq 'name_1'
+      expect(user.username).to eq 'username_1'
+    end
+
+    it 'returns another user by email' do
+      repo = UserRepository.new
+      user = repo.find_by_email('email_2')
+
+      expect(user.id).to eq 2
+      expect(user.email).to eq 'email_2'
+      expect(user.password).to eq 'password_2'
+      expect(user.name).to eq 'name_2'
+      expect(user.username).to eq 'username_2'
+    end
+
+    it 'returns nil if email does not exist' do
+      repo = UserRepository.new
+      user = repo.find_by_email('fake_email')
+      expect(user).to eq nil
+    end
+  end
+  
+  describe '#find_by_username' do
+    it 'returns a user by username' do
+      repo = UserRepository.new
+      user = repo.find_by_username('username_1')
+      
+      expect(user.id).to eq 1
+      expect(user.email).to eq 'email_1'
+      expect(user.password).to eq 'password_1'
+      expect(user.name).to eq 'name_1'
+      expect(user.username).to eq 'username_1'
+    end
+
+    it 'returns another user by username' do
+      repo = UserRepository.new
+      user = repo.find_by_username('username_2')
+
+      expect(user.id).to eq 2
+      expect(user.email).to eq 'email_2'
+      expect(user.password).to eq 'password_2'
+      expect(user.name).to eq 'name_2'
+      expect(user.username).to eq 'username_2'
+    end
+
+    it 'returns nil if email does not exist' do
+      repo = UserRepository.new
+      user = repo.find_by_username('fake_username')
+      expect(user).to eq nil
+    end
+  end
+
+  describe '#create' do
+    it "creates a new user in the database" do
+      user = User.new
+      user.email = 'new_email'
+      user.password = 'new_password'
+      user.name = 'new_name'
+      user.username = 'new_username'
+      
+      repo = UserRepository.new
+      repo.create(user)
+      
+      expect(repo.all).to include(
+        have_attributes(
+          id: 5,
+          email: 'new_email',
+          name: 'new_name',
+          username: 'new_username'
+        )
+      )
+    end
+    
+    it "encrypts the password" do
+      user = User.new
+      user.email = 'new_email'
+      user.password = 'new_password'
+      user.name = 'new_name'
+      user.username = 'new_username'
+      
+      repo = UserRepository.new
+      repo.create(user)
+
+      stored_user = repo.find(5)
+      expect(stored_user.password).not_to eq 'new_password'
+
+      stored_password = BCrypt::Password.new(stored_user.password)
+      expect(stored_password).to eq 'new_password'
+    end
+  end
+
+  describe '#username_exists?' do
+    it 'returns true if the username already exists in the database' do
+      repo = UserRepository.new
+      expect(repo.username_exists?('username_1')).to eq true
+    end
+    
+    it 'returns false if the username does not exist in the database' do
+      repo = UserRepository.new
+      expect(repo.username_exists?('new_username')).to eq false
+    end
+  end
+  
+  describe '#email_exists?' do
+    it 'returns true if the email already exists in the database' do
+      repo = UserRepository.new
+      expect(repo.email_exists?('email_1')).to eq true
+    end
+    
+    it 'returns false if the email does not exist in the database' do
+      repo = UserRepository.new
+      expect(repo.email_exists?('new_email')).to eq false
+    end
+  end
+
+  describe '#correct_password?' do
+    it 'returns true if email and password match' do
+      user = User.new
+      user.name = 'New Name'
+      user.username = 'user_name_1'
+      user.email = 'email1@email.com'
+      user.password = 'new_pass_123'
+
+      repo = UserRepository.new
+      repo.create(user)
+
+      result = repo.correct_password?('email1@email.com', 'new_pass_123')
+      expect(result).to eq true
+    end
+    
+    it 'returns false if email and password do not match' do
+      user = User.new
+      user.name = 'New Name'
+      user.username = 'user_name_1'
+      user.email = 'email1@email.com'
+      user.password = 'new_pass_123'
+  
+      repo = UserRepository.new
+      repo.create(user)
+  
+      result = repo.correct_password?('email1@email.com', 'new_pass_321')
+      expect(result).to eq false
+    end
+  end
+end
diff --git a/views/index.erb b/views/index.erb
new file mode 100644
index 0000000000..dc6492df5f
--- /dev/null
+++ b/views/index.erb
@@ -0,0 +1,25 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <h1>CHITTER!!1!!!11!</h1>
+        <% if @user.nil? %>
+            <p><a href="/sign-up">Sign Up</a></p>
+            <p><a href="/login">Log In</a></p>
+        <% else %>
+            <p><a href="/logout">Log Out</a></p>
+            <p><a href="/new-peep">Add new peep</a></p>
+        <% end %>
+
+        <h2>All Peeps</h2>
+        <% @peeps.each do |peep| %>
+            <div class="peep-container">
+                <p id="names"><%=peep.user.name %>:  @<%=peep.user.username %></p>
+                <p id="content"><%=peep.content %></p>
+                <p id="time"><%=peep.formatted_time %></p>
+            <br>
+            </div>
+        <% end %>
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/login.erb b/views/login.erb
new file mode 100644
index 0000000000..9273176edf
--- /dev/null
+++ b/views/login.erb
@@ -0,0 +1,16 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Log In</h1>
+        <form action="/login" method="POST">
+            <label>Email: </label>
+            <input type="text" name="email">
+            <label>Password: </label>
+            <input type="text" name="password">
+            <input type="submit">
+        </form>
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/login_failure.erb b/views/login_failure.erb
new file mode 100644
index 0000000000..bf79ea17f2
--- /dev/null
+++ b/views/login_failure.erb
@@ -0,0 +1,11 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Error!</h1>
+        <h2>One or more of your inputs was invalid</h2>
+        <p><a href="/login">Try again</a></p>
+    </body>
+</html>
diff --git a/views/login_success.erb b/views/login_success.erb
new file mode 100644
index 0000000000..4c7614ac52
--- /dev/null
+++ b/views/login_success.erb
@@ -0,0 +1,10 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Success!</h1>
+        <h2>Hello <%=@user.name%>! You are logged in as <%=@user.username%></h2>
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/new_peep.erb b/views/new_peep.erb
new file mode 100644
index 0000000000..1af89e2c06
--- /dev/null
+++ b/views/new_peep.erb
@@ -0,0 +1,14 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>New Peep</h1>
+        <form action="/new-peep" method="POST">
+            <label>PEEP: </label>
+            <input type="textarea" name="content">
+            <input type="submit">
+        </form>
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/new_peep_failure.erb b/views/new_peep_failure.erb
new file mode 100644
index 0000000000..f6a5ea4901
--- /dev/null
+++ b/views/new_peep_failure.erb
@@ -0,0 +1,12 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Error!</h1>
+        <h2>One or more of your inputs was invalid</h2>
+        <p><a href="/new-peep">Try again</a></p>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/new_peep_success.erb b/views/new_peep_success.erb
new file mode 100644
index 0000000000..2dfb64d81f
--- /dev/null
+++ b/views/new_peep_success.erb
@@ -0,0 +1,11 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Success!</h1>
+        <h2>You added a new peep!</h2>
+        <p><a href="/new-peep">Add another peep</a></p>
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/sign_up.erb b/views/sign_up.erb
new file mode 100644
index 0000000000..a33a321e64
--- /dev/null
+++ b/views/sign_up.erb
@@ -0,0 +1,20 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Sign Up</h1>
+        <form action="/sign-up" method="POST">
+            <label>Email: </label>
+            <input type="text" name="email">
+            <label>Password: </label>
+            <input type="text" name="password">
+            <label>Name: </label>
+            <input type="text" name="name">
+            <label>Username: </label>
+            <input type="text" name="username">
+            <input type="submit">
+        </form>
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/sign_up_failure.erb b/views/sign_up_failure.erb
new file mode 100644
index 0000000000..83a80f39b4
--- /dev/null
+++ b/views/sign_up_failure.erb
@@ -0,0 +1,12 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Error!</h1>
+        <h2>One or more of your inputs was invalid</h2>
+        <p><a href="/sign-up">Try again</a></p>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/views/sign_up_success.erb b/views/sign_up_success.erb
new file mode 100644
index 0000000000..6acd6b5cfe
--- /dev/null
+++ b/views/sign_up_success.erb
@@ -0,0 +1,10 @@
+<html>
+    <head>
+        <link rel="stylesheet" href="/styles.css">
+    </head>
+    <body>
+        <p><a href="/">Back to homepage</a></p>
+        <h1>Success!</h1>
+        <h2>Thanks for signing up!</h2>
+    </body>
+</html>
\ No newline at end of file