Files
clawmates/crates/cm-auth/src/bootstrap.rs
T
Omar SobhandClaude Fable 5 447f7039d8 Compose: production README, env knobs, first-owner bootstrap — deployed & live
Made the Docker Compose route turn-key for a real self-host, then stood
the whole stack up and drove a live chat through it.

- First-owner bootstrap (cm-auth::bootstrap_owner): a fresh local-auth
  install has no users and no signup route, so the initial Owner +
  workspace are provisioned ONCE from CLAWMATES_BOOTSTRAP_* env on first
  boot — idempotent, never clobbers an existing install (keys on 'any
  workspace exists'). Two real-Postgres tests (creates + signs in;
  second call is a no-op). Wired into server boot, guarded on a
  non-empty password
- deploy/compose/README.md: full production bring-up — services, the
  security topology, every config knob, Anthropic vs local-LLM, the
  broker-key backup, ops, and TLS/SSE proxy notes
- .env.example fleshed out (bootstrap, LLM, auth mode, OTLP); compose
  uses optional env_file so only the knobs you set are injected (unset
  options never override clawmates.toml with empty strings)
- volume-init one-shot chowns the broker's named volumes so the non-root
  scratch broker can write its socket + generated master key

Deployed locally and verified end to end: all 5 containers healthy,
broker generated its key, server bootstrapped owner@…, login + /api/user/me
work, and a real message streamed a live Anthropic response through the
gateway. Captured screenshots of login, workspace home, chat, and the
Computer panel.

166 Rust tests (+2 bootstrap).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-06-10 14:06:15 -05:00

68 lines
2.2 KiB
Rust

//! First-owner bootstrap for self-hosted (local-auth) deployments.
//!
//! There is no public signup route — a Clawmates workspace is invite-only
//! once it exists. But a brand-new install has nobody to invite anyone,
//! so the first Owner is provisioned from configuration on first boot.
//! The operation keys on "does ANY workspace exist yet", so it runs
//! exactly once no matter how many times the server restarts.
use cm_domain::{Role, User, UserId, Workspace, WorkspaceId};
use crate::{AuthError, AuthService};
/// Provisions the initial workspace + Owner if the install is empty.
/// Returns `true` when it created them, `false` when a workspace already
/// existed (the common restart case). Never modifies an existing install.
pub async fn bootstrap_owner(
pool: &sqlx::PgPool,
workspace_name: &str,
owner_email: &str,
owner_password: &str,
starter_credits: i64,
) -> Result<bool, AuthError> {
let existing: i64 = sqlx::query_scalar("SELECT count(*) FROM workspaces")
.fetch_one(pool)
.await?;
if existing > 0 {
return Ok(false);
}
let workspace = Workspace {
id: WorkspaceId::new(),
name: workspace_name.to_owned(),
plan: "team".into(),
};
cm_db::repo::workspaces::insert(pool, &workspace)
.await
.map_err(map_db)?;
let owner = User {
id: UserId::new(),
workspace_id: workspace.id,
email: owner_email.to_owned(),
role: Role::Owner,
display_name: owner_email.split('@').next().unwrap_or("owner").to_owned(),
created_at: time::OffsetDateTime::now_utc(),
};
cm_db::repo::users::insert(pool, &owner)
.await
.map_err(map_db)?;
AuthService::new(pool.clone())
.set_password(owner.id, owner_password)
.await?;
if starter_credits > 0 {
cm_db::repo::credits::add_lot(pool, workspace.id, starter_credits, "bootstrap")
.await
.map_err(map_db)?;
}
Ok(true)
}
fn map_db(error: cm_db::DbError) -> AuthError {
match error {
cm_db::DbError::Other(e) => AuthError::Db(e),
other => AuthError::Hashing(other.to_string()),
}
}