diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..78c265b2
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,83 @@
+name: Release CLI Binaries
+
+permissions:
+  contents: write
+
+on:
+  release:
+    types: [published]
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  build-and-upload:
+    name: Build & upload (${{ matrix.target }})
+    env:
+      SQLX_OFFLINE: true
+    strategy:
+      matrix:
+        include:
+          - os: ubuntu-latest
+            target: x86_64-unknown-linux-gnu
+            asset_os: linux
+            asset_arch: x86_64
+          - os: macos-latest
+            target: x86_64-apple-darwin
+            asset_os: darwin
+            asset_arch: x86_64
+          - os: macos-latest
+            target: aarch64-apple-darwin
+            asset_os: darwin
+            asset_arch: aarch64
+    runs-on: ${{ matrix.os }}
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Install Rust toolchain
+        uses: actions-rs/toolchain@v1
+        with:
+          toolchain: stable
+          target: ${{ matrix.target }}
+          override: true
+
+      - name: Cache cargo registry
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/registry
+          key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-cargo-registry-
+
+      - name: Cache cargo index
+        uses: actions/cache@v4
+        with:
+          path: ~/.cargo/git
+          key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            ${{ runner.os }}-cargo-index-
+
+      - name: Cache target directory
+        uses: actions/cache@v4
+        with:
+          path: target
+          key: release-${{ runner.os }}-target-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
+          restore-keys: |
+            release-${{ runner.os }}-target-${{ matrix.target }}-
+
+      - name: Build stacker-cli (release)
+        run: cargo build --release --target ${{ matrix.target }} --bin stacker-cli --verbose
+
+      - name: Package binary
+        run: |
+          VERSION="${GITHUB_REF_NAME#v}"
+          ASSET_NAME="stacker-v${VERSION}-${{ matrix.asset_arch }}-${{ matrix.asset_os }}.tar.gz"
+          mkdir -p staging
+          cp target/${{ matrix.target }}/release/stacker-cli staging/stacker
+          tar -czf "${ASSET_NAME}" -C staging .
+          echo "ASSET_NAME=${ASSET_NAME}" >> "$GITHUB_ENV"
+
+      - name: Upload release asset
+        uses: softprops/action-gh-release@v2
+        with:
+          files: ${{ env.ASSET_NAME }}
diff --git a/.gitignore b/.gitignore
index 82bf7858..a777a745 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,5 +7,6 @@ configuration.yaml.backup
 configuration.yaml.orig
 .vscode/
 .env
+docker/local/
 docs/*.sql
 config-to-validate.yaml
diff --git a/Cargo.toml b/Cargo.toml
index c268c04e..0a4f0314 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "stacker"
-version = "0.2.3"
+version = "0.2.4"
 edition = "2021"
 default-run= "server"
 
diff --git a/README.md b/README.md
index 009bf2e8..a510321e 100644
--- a/README.md
+++ b/README.md
@@ -23,13 +23,13 @@ Stacker is a platform for turning any project into a deployable Docker stack. Ad
 
 ```
 ┌──────────────┐         ┌──────────────────┐         ┌─────────────────────┐
-│  Stacker CLI │────────►│  Stacker Server   │────────►│  Status Panel Agent │
-│              │  REST   │                   │  queue  │  (on target server) │
-│  stacker.yml │  API    │  Stack Builder UI │  pull   │                     │
-│  init/deploy │         │  48+ MCP tools    │◄────────│  health / logs /    │
-│  status/logs │         │  Vault · AMQP     │  HMAC   │  restart / exec /   │
+│  Stacker CLI │────────►│  Stacker Server  │────────►│  Status Panel Agent │
+│              │  REST   │                  │  queue  │  (on target server) │
+│  stacker.yml │  API    │  Stack Builder UI│  pull   │                     │
+│  init/deploy │         │  48+ MCP tools   │◄────────│  health / logs /    │
+│  status/logs │         │  Vault · AMQP    │  HMAC   │  restart / exec /   │
 └──────────────┘         └──────────────────┘         │  deploy_app / proxy │
-                                │                      └─────────────────────┘
+                                │                     └─────────────────────┘
                                 ▼
                     Terraform + Ansible ──► Cloud
                     (Hetzner, DO, AWS, Linode)
diff --git a/docker/dev/.env b/docker/dev/.env
index 892f3064..53d4f824 100644
--- a/docker/dev/.env
+++ b/docker/dev/.env
@@ -7,9 +7,11 @@ POSTGRES_DB=stacker
 POSTGRES_PORT=5432
 
 # Vault Configuration
-VAULT_ADDRESS=http://127.0.0.1:8200
+VAULT_ADDRESS=http://vault2.try.direct:8200
 VAULT_TOKEN=your_vault_token_here
 VAULT_AGENT_PATH_PREFIX=agent
+VAULT_API_PREFIX=v1
+VAULT_SSH_KEY_PATH_PREFIX=users
 
 ### 10.3 Environment Variables Required
 # User Service integration
diff --git a/src/helpers/vault.rs b/src/helpers/vault.rs
index d468c4a7..3a0a6c21 100644
--- a/src/helpers/vault.rs
+++ b/src/helpers/vault.rs
@@ -167,13 +167,13 @@ impl VaultClient {
 
     // ============ SSH Key Management Methods ============
 
-    /// Build the Vault path for SSH keys: {base}/v1/secret/users/{user_id}/ssh_keys/{server_id}
+    /// Build the Vault API URL for SSH keys (KV v1).
+    /// Path: `{address}/{api_prefix}/secret/{prefix}/{user_id}/ssh_keys/{server_id}`
     fn ssh_key_path(&self, user_id: &str, server_id: i32) -> String {
         let base = self.address.trim_end_matches('/');
         let api_prefix = self.api_prefix.trim_matches('/');
         let prefix = self.ssh_key_path_prefix.trim_matches('/');
 
-        // Path without 'data' segment (KV v1 or custom mount)
         if api_prefix.is_empty() {
             format!(
                 "{}/secret/{}/{}/ssh_keys/{}",
@@ -219,13 +219,11 @@ impl VaultClient {
         let path = self.ssh_key_path(user_id, server_id);
 
         let payload = json!({
-            "data": {
-                "public_key": public_key,
-                "private_key": private_key,
-                "user_id": user_id,
-                "server_id": server_id,
-                "created_at": chrono::Utc::now().to_rfc3339()
-            }
+            "public_key": public_key,
+            "private_key": private_key,
+            "user_id": user_id,
+            "server_id": server_id,
+            "created_at": chrono::Utc::now().to_rfc3339()
         });
 
         self.client
@@ -244,7 +242,7 @@ impl VaultClient {
                 format!("Vault error: {}", e)
             })?;
 
-        // Return the vault path for storage in database
+        // Return the logical vault path for storage in database
         let vault_key_path = format!(
             "secret/{}/{}/ssh_keys/{}",
             self.ssh_key_path_prefix.trim_matches('/'),
@@ -293,7 +291,7 @@ impl VaultClient {
                 format!("Vault parse error: {}", e)
             })?;
 
-        vault_response["data"]["data"]["private_key"]
+        vault_response["data"]["private_key"]
             .as_str()
             .map(|s| s.to_string())
             .ok_or_else(|| {
@@ -339,7 +337,7 @@ impl VaultClient {
                 format!("Vault parse error: {}", e)
             })?;
 
-        vault_response["data"]["data"]["public_key"]
+        vault_response["data"]["public_key"]
             .as_str()
             .map(|s| s.to_string())
             .ok_or_else(|| {
diff --git a/src/routes/project/compose.rs b/src/routes/project/compose.rs
index 3cc7d8ae..a36f8ff1 100644
--- a/src/routes/project/compose.rs
+++ b/src/routes/project/compose.rs
@@ -27,7 +27,7 @@ pub async fn add(
 
     DcBuilder::new(project)
         .build()
-        .map_err(|err| JsonResponse::<models::Project>::build().internal_server_error(err))
+        .map_err(|err| JsonResponse::<models::Project>::build().bad_request(err))
         .map(|fc| JsonResponse::build().set_id(id).set_item(fc).ok("Success"))
 }
 
@@ -50,6 +50,6 @@ pub async fn admin(
 
     DcBuilder::new(project)
         .build()
-        .map_err(|err| JsonResponse::<models::Project>::build().internal_server_error(err))
+        .map_err(|err| JsonResponse::<models::Project>::build().bad_request(err))
         .map(|fc| JsonResponse::build().set_id(id).set_item(fc).ok("Success"))
 }
diff --git a/src/routes/server/ssh_key.rs b/src/routes/server/ssh_key.rs
index 30dbdf53..56a03cb0 100644
--- a/src/routes/server/ssh_key.rs
+++ b/src/routes/server/ssh_key.rs
@@ -211,14 +211,24 @@ pub async fn get_public_key(
             .not_found("No active SSH key found for this server"));
     }
 
+    if server.vault_key_path.is_none() {
+        return Err(JsonResponse::<PublicKeyResponse>::build()
+            .bad_request("SSH key is not stored in Vault (Vault was unavailable when the key was generated). Please delete this key and generate a new one."));
+    }
+
     let public_key = vault_client
         .get_ref()
         .fetch_ssh_public_key(&user.id, server_id)
         .await
         .map_err(|e| {
             tracing::error!("Failed to fetch public key from Vault: {}", e);
-            JsonResponse::<PublicKeyResponse>::build()
-                .internal_server_error("Failed to retrieve public key")
+            if e.to_lowercase().contains("not found") {
+                JsonResponse::<PublicKeyResponse>::build()
+                    .not_found("SSH key not found in Vault. The key may have been lost or Vault was restored without its data. Please delete this key and generate a new one.")
+            } else {
+                JsonResponse::<PublicKeyResponse>::build()
+                    .bad_request("Failed to retrieve SSH key from Vault. Please try again or regenerate the key.")
+            }
         })?;
 
     let response = PublicKeyResponse {
@@ -311,6 +321,19 @@ pub async fn validate_key(
             .ok("Validation failed"));
     }
 
+    if server.vault_key_path.is_none() {
+        let response = ValidateResponse {
+            valid: false,
+            server_id,
+            srv_ip: server.srv_ip.clone(),
+            message: "SSH key is not stored in Vault (Vault was unavailable when the key was generated). Please delete this key and generate a new one.".to_string(),
+            ..Default::default()
+        };
+        return Ok(JsonResponse::build()
+            .set_item(Some(response))
+            .ok("Validation failed"));
+    }
+
     // Verify we have the server IP
     let srv_ip = match &server.srv_ip {
         Some(ip) if !ip.is_empty() => ip.clone(),
diff --git a/tests/common/mod.rs b/tests/common/mod.rs
index 555fec29..3006212c 100644
--- a/tests/common/mod.rs
+++ b/tests/common/mod.rs
@@ -4,6 +4,7 @@ use stacker::configuration::{get_configuration, DatabaseSettings, Settings};
 use stacker::forms;
 use stacker::helpers::AgentPgPool;
 use std::net::TcpListener;
+use wiremock::MockServer;
 
 pub async fn spawn_app_with_configuration(mut configuration: Settings) -> Option<TestApp> {
     let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port");
@@ -85,6 +86,106 @@ pub struct TestApp {
     pub db_pool: PgPool,
 }
 
+pub struct TestAppWithVault {
+    pub address: String,
+    pub db_pool: PgPool,
+    pub vault_server: MockServer,
+}
+
+/// Spawn the full app with a mock Vault server.
+/// The returned `vault_server` is a wiremock MockServer — mount expectations on it
+/// before calling API endpoints that touch Vault.
+pub async fn spawn_app_with_vault() -> Option<TestAppWithVault> {
+    let mut configuration = get_configuration().expect("Failed to get configuration");
+
+    // Mock auth server
+    let auth_listener = std::net::TcpListener::bind("127.0.0.1:0")
+        .expect("Failed to bind port for testing auth server");
+    configuration.auth_url = format!(
+        "http://127.0.0.1:{}/me",
+        auth_listener.local_addr().unwrap().port()
+    );
+    let _ = tokio::spawn(mock_auth_server(auth_listener));
+    tokio::time::sleep(std::time::Duration::from_millis(200)).await;
+
+    // Mock Vault server
+    let vault_server = MockServer::start().await;
+    configuration.vault.address = vault_server.uri();
+    configuration.vault.token = "test-vault-token".to_string();
+    configuration.vault.api_prefix = "v1".to_string();
+    configuration.vault.ssh_key_path_prefix = Some("users".to_string());
+
+    configuration.database.database_name = uuid::Uuid::new_v4().to_string();
+
+    let connection_pool = match configure_database(&configuration.database).await {
+        Ok(pool) => pool,
+        Err(err) => {
+            eprintln!("Skipping tests: failed to connect to postgres: {}", err);
+            return None;
+        }
+    };
+
+    let app_listener = std::net::TcpListener::bind("127.0.0.1:0").expect("Failed to bind app port");
+    let port = app_listener.local_addr().unwrap().port();
+    let address = format!("http://127.0.0.1:{}", port);
+
+    let agent_pool = AgentPgPool::new(connection_pool.clone());
+    let server = stacker::startup::run(app_listener, connection_pool.clone(), agent_pool, configuration)
+        .await
+        .expect("Failed to bind address.");
+    let _ = tokio::spawn(server);
+
+    Some(TestAppWithVault {
+        address,
+        db_pool: connection_pool,
+        vault_server,
+    })
+}
+
+/// Insert a minimal project into the DB and return its id.
+/// Required because server.project_id has a FK constraint to project(id).
+pub async fn create_test_project(pool: &PgPool, user_id: &str) -> i32 {
+    sqlx::query(
+        r#"INSERT INTO project (stack_id, user_id, name, body, created_at, updated_at)
+        VALUES (gen_random_uuid(), $1, 'Test Project', '{}', NOW(), NOW())
+        RETURNING id"#,
+    )
+    .bind(user_id)
+    .fetch_one(pool)
+    .await
+    .map(|row| {
+        use sqlx::Row;
+        row.get::<i32, _>("id")
+    })
+    .expect("Failed to insert test project")
+}
+
+/// Insert a test server with specific SSH key state and return its id.
+pub async fn create_test_server(
+    pool: &PgPool,
+    user_id: &str,
+    project_id: i32,
+    key_status: &str,
+    vault_key_path: Option<&str>,
+) -> i32 {
+    sqlx::query(
+        r#"INSERT INTO server (user_id, project_id, connection_mode, key_status, vault_key_path, created_at, updated_at)
+        VALUES ($1, $2, 'ssh', $3, $4, NOW(), NOW())
+        RETURNING id"#,
+    )
+    .bind(user_id)
+    .bind(project_id)
+    .bind(key_status)
+    .bind(vault_key_path)
+    .fetch_one(pool)
+    .await
+    .map(|row| {
+        use sqlx::Row;
+        row.get::<i32, _>("id")
+    })
+    .expect("Failed to insert test server")
+}
+
 #[get("")]
 async fn mock_auth() -> actix_web::Result<impl Responder> {
     println!("Mock auth endpoint called - returning test user");
diff --git a/tests/server_ssh.rs b/tests/server_ssh.rs
index f012a9a8..f0986cb7 100644
--- a/tests/server_ssh.rs
+++ b/tests/server_ssh.rs
@@ -1,179 +1,454 @@
 mod common;
 
-use serde_json::json;
+use serde_json::{json, Value};
+use wiremock::matchers::{method, path_regex};
+use wiremock::{Mock, ResponseTemplate};
 
-// Test SSH key generation for server
-// Run: cargo t --test server_ssh -- --nocapture --show-output
+// ─────────────────────────────────────────────────────────────────────────────
+// Helpers
+// ─────────────────────────────────────────────────────────────────────────────
 
-/// Test that the server list endpoint returns success
+/// Vault path pattern for SSH keys: /v1/secret/users/{user_id}/ssh_keys/{server_id}
+fn vault_ssh_path_regex(user_id: &str, server_id: i32) -> String {
+    format!(
+        r"/v1/secret/users/{}/ssh_keys/{}",
+        user_id, server_id
+    )
+}
+
+/// Successful Vault GET response body for a KV v1 SSH key read.
+fn vault_key_response(public_key: &str, private_key: &str) -> serde_json::Value {
+    json!({
+        "data": {
+            "public_key": public_key,
+            "private_key": private_key
+        }
+    })
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Tests: GET /server/{id}/ssh-key/public
+// ─────────────────────────────────────────────────────────────────────────────
+
+/// Server has key_status=active but vault_key_path=NULL (Vault failed during generate).
+/// Must return 400, not 500.
 #[tokio::test]
-async fn get_server_list() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_get_public_key_vault_path_null_returns_400() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "active",
+        None, // vault_key_path is NULL
+    )
+    .await;
 
-    let response = client
-        .get(&format!("{}/server", &app.address))
+    let client = reqwest::Client::new();
+    let resp = client
+        .get(&format!("{}/server/{}/ssh-key/public", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
 
-    // Should return 200 OK (empty list is fine)
-    assert!(response.status().is_success());
+    assert_eq!(resp.status().as_u16(), 400, "Should be 400, not 500");
+    let body: Value = resp.json().await.unwrap();
+    let msg = body["message"].as_str().unwrap_or("");
+    assert!(
+        msg.to_lowercase().contains("vault") || msg.to_lowercase().contains("regenerate") || msg.to_lowercase().contains("delete"),
+        "Error message should mention Vault or remediation: {}", msg
+    );
+    // Vault server must NOT have been called (no vault_key_path to use)
+    assert_eq!(app.vault_server.received_requests().await.unwrap().len(), 0);
 }
 
-/// Test that getting a non-existent server returns 404
+/// Server has key_status=active and vault_key_path set, but Vault returns 404.
+/// Must return 404 (key lost from Vault), not 500.
 #[tokio::test]
-async fn get_server_not_found() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_get_public_key_vault_returns_404_propagates_as_404() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "active",
+        Some(&format!("secret/users/test_user_id/ssh_keys/{}", 999)),
+    )
+    .await;
+
+    // Mount Vault mock: GET → 404
+    Mock::given(method("GET"))
+        .and(path_regex(vault_ssh_path_regex("test_user_id", server_id)))
+        .respond_with(ResponseTemplate::new(404).set_body_json(json!({"errors": []})))
+        .mount(&app.vault_server)
+        .await;
 
-    let response = client
-        .get(&format!("{}/server/99999", &app.address))
+    let client = reqwest::Client::new();
+    let resp = client
+        .get(&format!("{}/server/{}/ssh-key/public", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
 
-    // Should return 404 for non-existent server
-    assert_eq!(response.status().as_u16(), 404);
+    assert_eq!(resp.status().as_u16(), 404, "Should be 404 when Vault returns 404");
+    let body: Value = resp.json().await.unwrap();
+    let msg = body["message"].as_str().unwrap_or("");
+    assert!(
+        msg.to_lowercase().contains("vault") || msg.to_lowercase().contains("regenerate"),
+        "Error message should mention Vault: {}", msg
+    );
 }
 
-/// Test that generating SSH key requires authentication
+/// Server has key_status="none" — no key has been generated yet.
+/// Must return 404.
 #[tokio::test]
-async fn generate_ssh_key_requires_auth() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_get_public_key_no_active_key_returns_404() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "none",
+        None,
+    )
+    .await;
 
-    let response = client
-        .post(&format!("{}/server/1/ssh-key/generate", &app.address))
+    let client = reqwest::Client::new();
+    let resp = client
+        .get(&format!("{}/server/{}/ssh-key/public", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
 
-    // Should require authentication (401 or 403)
-    let status = response.status().as_u16();
-    assert!(status == 401 || status == 403 || status == 404);
+    assert_eq!(resp.status().as_u16(), 404);
 }
 
-/// Test that uploading SSH key validates input
+/// Happy path: active key, vault_key_path set, Vault returns the key successfully.
 #[tokio::test]
-async fn upload_ssh_key_validates_input() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_get_public_key_success() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "active",
+        Some(&format!("secret/users/test_user_id/ssh_keys/{}", 0)), // path value doesn't matter for routing
+    )
+    .await;
 
-    // Send invalid key format
-    let invalid_data = json!({
-        "public_key": "not-a-valid-key",
-        "private_key": "also-not-valid"
-    });
+    let expected_pub_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestPublicKey";
 
-    let response = client
-        .post(&format!("{}/server/1/ssh-key/upload", &app.address))
-        .header("Content-Type", "application/json")
-        .body(invalid_data.to_string())
+    Mock::given(method("GET"))
+        .and(path_regex(vault_ssh_path_regex("test_user_id", server_id)))
+        .respond_with(
+            ResponseTemplate::new(200).set_body_json(vault_key_response(
+                expected_pub_key,
+                "-----BEGIN OPENSSH PRIVATE KEY-----\ntest\n-----END OPENSSH PRIVATE KEY-----",
+            )),
+        )
+        .mount(&app.vault_server)
+        .await;
+
+    let client = reqwest::Client::new();
+    let resp = client
+        .get(&format!("{}/server/{}/ssh-key/public", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
 
-    // Should reject invalid key format (400 or 401/403 if auth required first)
-    let status = response.status().as_u16();
-    assert!(status == 400 || status == 401 || status == 403 || status == 404);
+    assert_eq!(resp.status().as_u16(), 200);
+    let body: Value = resp.json().await.unwrap();
+    assert_eq!(
+        body["item"]["public_key"].as_str().unwrap_or(""),
+        expected_pub_key,
+        "Response should contain the public key from Vault"
+    );
 }
 
-/// Test that getting public key for non-existent server returns error
+// ─────────────────────────────────────────────────────────────────────────────
+// Tests: POST /server/{id}/ssh-key/generate
+// ─────────────────────────────────────────────────────────────────────────────
+
+/// When Vault is unavailable during generate, the private key MUST be returned
+/// inline in the response, and the DB must have key_status=active + vault_key_path=NULL.
 #[tokio::test]
-async fn get_public_key_not_found() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_generate_key_vault_down_returns_private_key_inline() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "none",
+        None,
+    )
+    .await;
 
-    let response = client
-        .get(&format!("{}/server/99999/ssh-key/public", &app.address))
+    // Vault is down — POST returns 500
+    Mock::given(method("POST"))
+        .and(path_regex(vault_ssh_path_regex("test_user_id", server_id)))
+        .respond_with(ResponseTemplate::new(500).set_body_string("vault unavailable"))
+        .mount(&app.vault_server)
+        .await;
+
+    let client = reqwest::Client::new();
+    let resp = client
+        .post(&format!("{}/server/{}/ssh-key/generate", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
+
+    assert_eq!(resp.status().as_u16(), 200, "Generate should succeed even when Vault is down");
+    let body: Value = resp.json().await.unwrap();
 
-    // Should return 404
-    let status = response.status().as_u16();
-    assert!(status == 404 || status == 401 || status == 403);
+    // Private key must be returned inline so user can save it
+    assert!(
+        body["item"]["private_key"].is_string(),
+        "Private key must be returned inline when Vault is unavailable"
+    );
+    assert!(
+        body["item"]["public_key"].is_string(),
+        "Public key must also be present"
+    );
+
+    // DB: key_status must be "active" and vault_key_path must be NULL
+    let row = sqlx::query("SELECT key_status, vault_key_path FROM server WHERE id = $1")
+        .bind(server_id)
+        .fetch_one(&app.db_pool)
+        .await
+        .expect("DB query failed");
+    use sqlx::Row;
+    let db_key_status: String = row.get("key_status");
+    let db_vault_path: Option<String> = row.get("vault_key_path");
+    assert_eq!(db_key_status, "active");
+    assert!(
+        db_vault_path.is_none(),
+        "vault_key_path must be NULL when Vault store failed"
+    );
 }
 
-/// Test that deleting SSH key for non-existent server returns error
+/// Happy path: Vault is available — key is stored, no private key in response, vault_key_path saved.
 #[tokio::test]
-async fn delete_ssh_key_not_found() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_generate_key_success_stores_in_vault_no_private_key_exposed() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "none",
+        None,
+    )
+    .await;
 
-    let response = client
-        .delete(&format!("{}/server/99999/ssh-key", &app.address))
+    // Vault is up — POST returns 204
+    Mock::given(method("POST"))
+        .and(path_regex(vault_ssh_path_regex("test_user_id", server_id)))
+        .respond_with(ResponseTemplate::new(204))
+        .mount(&app.vault_server)
+        .await;
+
+    let client = reqwest::Client::new();
+    let resp = client
+        .post(&format!("{}/server/{}/ssh-key/generate", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
+
+    assert_eq!(resp.status().as_u16(), 200);
+    let body: Value = resp.json().await.unwrap();
 
-    // Should return 404 or auth error
-    let status = response.status().as_u16();
-    assert!(status == 404 || status == 401 || status == 403);
+    // Private key must NOT be in response when Vault worked
+    assert!(
+        body["item"]["private_key"].is_null() || !body["item"]["private_key"].is_string(),
+        "Private key must NOT be returned when Vault stored it successfully"
+    );
+    assert!(body["item"]["public_key"].is_string(), "Public key must be present");
+
+    // DB: vault_key_path must be set
+    let row = sqlx::query("SELECT key_status, vault_key_path FROM server WHERE id = $1")
+        .bind(server_id)
+        .fetch_one(&app.db_pool)
+        .await
+        .expect("DB query failed");
+    use sqlx::Row;
+    let db_key_status: String = row.get("key_status");
+    let db_vault_path: Option<String> = row.get("vault_key_path");
+    assert_eq!(db_key_status, "active");
+    assert!(
+        db_vault_path.is_some(),
+        "vault_key_path must be saved in DB after successful Vault store"
+    );
 }
 
-/// Test server update endpoint
+/// Generating a key when one is already active must return 400.
 #[tokio::test]
-async fn update_server_not_found() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_generate_key_already_active_returns_400() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "active",
+        Some("secret/users/test_user_id/ssh_keys/1"),
+    )
+    .await;
+
     let client = reqwest::Client::new();
+    let resp = client
+        .post(&format!("{}/server/{}/ssh-key/generate", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
+        .send()
+        .await
+        .expect("request failed");
+
+    assert_eq!(resp.status().as_u16(), 400);
+    // Vault must NOT have been called
+    assert_eq!(app.vault_server.received_requests().await.unwrap().len(), 0);
+}
 
-    let update_data = json!({
-        "name": "My Server",
-        "connection_mode": "ssh"
-    });
+// ─────────────────────────────────────────────────────────────────────────────
+// Tests: DELETE /server/{id}/ssh-key
+// ─────────────────────────────────────────────────────────────────────────────
 
-    let response = client
-        .put(&format!("{}/server/99999", &app.address))
-        .header("Content-Type", "application/json")
-        .body(update_data.to_string())
+/// Deleting an active key must call Vault DELETE, reset key_status to "none",
+/// and clear vault_key_path in DB.
+#[tokio::test]
+async fn test_delete_key_clears_vault_and_db() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
+        None => return,
+    };
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "active",
+        Some(&format!("secret/users/test_user_id/ssh_keys/{}", 0)),
+    )
+    .await;
+
+    Mock::given(method("DELETE"))
+        .and(path_regex(vault_ssh_path_regex("test_user_id", server_id)))
+        .respond_with(ResponseTemplate::new(204))
+        .mount(&app.vault_server)
+        .await;
+
+    let client = reqwest::Client::new();
+    let resp = client
+        .delete(&format!("{}/server/{}/ssh-key", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
 
-    // Should return 404 for non-existent server
-    let status = response.status().as_u16();
-    assert!(status == 404 || status == 401 || status == 403);
+    assert_eq!(resp.status().as_u16(), 200);
+
+    let row = sqlx::query("SELECT key_status, vault_key_path FROM server WHERE id = $1")
+        .bind(server_id)
+        .fetch_one(&app.db_pool)
+        .await
+        .expect("DB query failed");
+    use sqlx::Row;
+    let db_key_status: String = row.get("key_status");
+    let db_vault_path: Option<String> = row.get("vault_key_path");
+    assert_eq!(db_key_status, "none", "key_status must be reset to 'none'");
+    assert!(db_vault_path.is_none(), "vault_key_path must be cleared");
 }
 
-/// Test get servers by project endpoint
+/// Deleting when no key exists must return 400.
 #[tokio::test]
-async fn get_servers_by_project() {
-    let app = match common::spawn_app().await {
-        Some(app) => app,
+async fn test_delete_key_none_returns_400() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
         None => return,
     };
-    let client = reqwest::Client::new();
+    let project_id = common::create_test_project(&app.db_pool, "test_user_id").await;
+    let server_id = common::create_test_server(
+        &app.db_pool,
+        "test_user_id",
+        project_id,
+        "none",
+        None,
+    )
+    .await;
 
-    let response = client
-        .get(&format!("{}/server/project/1", &app.address))
+    let client = reqwest::Client::new();
+    let resp = client
+        .delete(&format!("{}/server/{}/ssh-key", &app.address, server_id))
+        .header("Authorization", "Bearer test-token")
         .send()
         .await
-        .expect("Failed to execute request.");
+        .expect("request failed");
+
+    assert_eq!(resp.status().as_u16(), 400);
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+// Tests: Unauthenticated access
+// ─────────────────────────────────────────────────────────────────────────────
+
+/// All SSH key endpoints must reject requests without a Bearer token.
+#[tokio::test]
+async fn test_ssh_key_endpoints_require_auth() {
+    let app = match common::spawn_app_with_vault().await {
+        Some(a) => a,
+        None => return,
+    };
+    let client = reqwest::Client::new();
+
+    let endpoints: &[(&str, &str)] = &[
+        ("GET",    "/server/1/ssh-key/public"),
+        ("POST",   "/server/1/ssh-key/generate"),
+        ("DELETE", "/server/1/ssh-key"),
+    ];
 
-    // Should return success or auth error
-    let status = response.status().as_u16();
-    assert!(status == 200 || status == 404 || status == 401 || status == 403);
+    for (verb, path) in endpoints {
+        let req = match *verb {
+            "GET"    => client.get(&format!("{}{}", &app.address, path)),
+            "POST"   => client.post(&format!("{}{}", &app.address, path)),
+            "DELETE" => client.delete(&format!("{}{}", &app.address, path)),
+            _        => unreachable!(),
+        };
+        let resp = req.send().await.expect("request failed");
+        let status = resp.status().as_u16();
+        assert!(
+            status == 400 || status == 401 || status == 403 || status == 404,
+            "{} {} without auth should return 400/401/403, got {}",
+            verb, path, status
+        );
+    }
 }
diff --git a/website/stacker.yml b/website/stacker.yml
index 4b1854a3..61a9d90f 100644
--- a/website/stacker.yml
+++ b/website/stacker.yml
@@ -2,6 +2,8 @@ name: website
 version: 1.0.0
 project:
   identity: stacker-website
+  _id: 44
+
 app:
   type: node
   path: '.'
@@ -12,26 +14,10 @@ services:
     image: ${DOCKER_USERNAME}/stacker-website:latest
     ports:
       - "3456:3456"
-    environment:
-      DB_PASSWORD: ${DB_PASSWORD}
-      OPENAI_API_KEY: ${OPENAI_API_KEY}
     volumes:
       - .:/app
-    depends_on:
-      - db
-  db:
-    name: db
-    image: postgres:14
-    environment:
-      POSTGRES_DB: stacker_db
-      POSTGRES_USER: stacker_user
-      POSTGRES_PASSWORD: ${DB_PASSWORD}
-    ports:
-      - "5432:5432"
-    volumes:
-      - db-data:/var/lib/postgresql/data
 
-monitors:
+monitoring:
   status_panel: true
   healthcheck:
     endpoint: /healthz
