diff --git a/cargo-shuttle/src/args.rs b/cargo-shuttle/src/args.rs
index 9976d3f6d..0ee9f896d 100644
--- a/cargo-shuttle/src/args.rs
+++ b/cargo-shuttle/src/args.rs
@@ -111,6 +111,8 @@ pub enum DeploymentCommand {
 pub enum ProjectCommand {
     /// create an environment for this project on shuttle
     New,
+    /// list all projects belonging to the calling account
+    List,
     /// remove this project environment from shuttle
     Rm,
     /// show the status of this project's environment on shuttle
diff --git a/cargo-shuttle/src/client.rs b/cargo-shuttle/src/client.rs
index 4f3c4c778..9fe67e5ef 100644
--- a/cargo-shuttle/src/client.rs
+++ b/cargo-shuttle/src/client.rs
@@ -122,6 +122,12 @@ impl Client {
         self.get(path).await
     }
 
+    pub async fn get_projects_list(&self) -> Result<Vec<project::Response>> {
+        let path = "/projects".to_string();
+
+        self.get(path).await
+    }
+
     pub async fn delete_project(&self, project: &ProjectName) -> Result<project::Response> {
         let path = format!("/projects/{}", project.as_str());
 
diff --git a/cargo-shuttle/src/lib.rs b/cargo-shuttle/src/lib.rs
index f6e7333e9..affec7a2a 100644
--- a/cargo-shuttle/src/lib.rs
+++ b/cargo-shuttle/src/lib.rs
@@ -99,6 +99,7 @@ impl Shuttle {
                     Command::Project(ProjectCommand::Status { follow }) => {
                         self.project_status(&client, follow).await
                     }
+                    Command::Project(ProjectCommand::List) => self.projects_list(&client).await,
                     Command::Project(ProjectCommand::Rm) => self.project_delete(&client).await,
                     _ => {
                         unreachable!("commands that don't need a client have already been matched")
@@ -521,6 +522,15 @@ impl Shuttle {
         Ok(())
     }
 
+    async fn projects_list(&self, client: &Client) -> Result<()> {
+        let projects = client.get_projects_list().await?;
+        let projects_table = project::get_table(&projects);
+
+        println!("{projects_table}");
+
+        Ok(())
+    }
+
     async fn project_status(&self, client: &Client, follow: bool) -> Result<()> {
         match follow {
             true => {
diff --git a/common/src/models/project.rs b/common/src/models/project.rs
index 171c4e46a..015bc2302 100644
--- a/common/src/models/project.rs
+++ b/common/src/models/project.rs
@@ -1,4 +1,7 @@
-use comfy_table::Color;
+use comfy_table::{
+    modifiers::UTF8_ROUND_CORNERS, presets::UTF8_FULL, Cell, CellAlignment, Color,
+    ContentArrangement, Table,
+};
 use crossterm::style::Stylize;
 use serde::{Deserialize, Serialize};
 use std::fmt::{Display, Formatter};
@@ -52,3 +55,39 @@ pub struct AdminResponse {
     pub project_name: String,
     pub account_name: String,
 }
+
+pub fn get_table(projects: &Vec<Response>) -> String {
+    if projects.is_empty() {
+        format!(
+            "{}\n",
+            "No projects are linked to this account".yellow().bold()
+        )
+    } else {
+        let mut table = Table::new();
+        table
+            .load_preset(UTF8_FULL)
+            .apply_modifier(UTF8_ROUND_CORNERS)
+            .set_content_arrangement(ContentArrangement::DynamicFullWidth)
+            .set_header(vec![
+                Cell::new("Project Name").set_alignment(CellAlignment::Center),
+                Cell::new("Status").set_alignment(CellAlignment::Center),
+            ]);
+
+        for project in projects.iter() {
+            table.add_row(vec![
+                Cell::new(&project.name),
+                Cell::new(&project.state)
+                    .fg(project.state.get_color())
+                    .set_alignment(CellAlignment::Center),
+            ]);
+        }
+
+        format!(
+            r#"
+These projects are linked to this account
+{}
+"#,
+            table,
+        )
+    }
+}
diff --git a/gateway/src/api/latest.rs b/gateway/src/api/latest.rs
index 9b7093f1a..accc19330 100644
--- a/gateway/src/api/latest.rs
+++ b/gateway/src/api/latest.rs
@@ -104,6 +104,23 @@ async fn get_project(
     Ok(AxumJson(response))
 }
 
+async fn get_projects_list(
+    State(RouterState { service, .. }): State<RouterState>,
+    _: User,
+) -> Result<AxumJson<Vec<project::Response>>, Error> {
+    let projects = service
+        .iter_projects_list()
+        .await?
+        .into_iter()
+        .map(|project| project::Response {
+            name: project.0.to_string(),
+            state: project.1.into(),
+        })
+        .collect();
+
+    Ok(AxumJson(projects))
+}
+
 #[instrument(skip_all, fields(%project))]
 async fn post_project(
     State(RouterState {
@@ -457,6 +474,7 @@ impl ApiBuilder {
         self.router = self
             .router
             .route("/", get(get_status))
+            .route("/projects", get(get_projects_list))
             .route(
                 "/projects/:project_name",
                 get(get_project).delete(delete_project).post(post_project),
diff --git a/gateway/src/service.rs b/gateway/src/service.rs
index 20ca92a05..5b5960f4a 100644
--- a/gateway/src/service.rs
+++ b/gateway/src/service.rs
@@ -276,6 +276,22 @@ impl GatewayService {
             .ok_or_else(|| Error::from_kind(ErrorKind::ProjectNotFound))
     }
 
+    pub async fn iter_projects_list(
+        &self,
+    ) -> Result<impl Iterator<Item = (ProjectName, Project)>, Error> {
+        let iter = query("SELECT project_name, project_state FROM projects")
+            .fetch_all(&self.db)
+            .await?
+            .into_iter()
+            .map(|row| {
+                (
+                    row.get("project_name"),
+                    row.get::<SqlxJson<Project>, _>("project_state").0,
+                )
+            });
+        Ok(iter)
+    }
+
     pub async fn update_project(
         &self,
         project_name: &ProjectName,