V1 Implementation Guide

Conman V1 Implementation Guide

Goal: Build a Git-backed configuration manager for DxFlow-style config repositories. API-only backend in Rust.

Architecture: Axum HTTP server exposing a REST API. Git operations delegated to a running gitaly-rs instance via gRPC (Tonic client). MongoDB stores workflow state, audit trails, and app metadata. Async job runner handles long-running operations (msuite, revalidation, deployments). Runtime profiles model URL/env vars/secrets/database/data configuration for environments and temp envs.

Tech Stack:

Component Choice Version
Language Rust edition 2024
HTTP framework Axum 0.8
gRPC client Tonic 0.12
Protobuf Prost 0.13
Async runtime Tokio 1.x
MongoDB driver mongodb (official) latest
Serialization serde + serde_json latest
Error handling thiserror latest
Password hashing argon2 latest
JWT jsonwebtoken latest
Time chrono latest
Tracing tracing + tracing-subscriber latest
UUID uuid latest (v7 generation)

Full scope: docs/conman-v1-scope.md and docs/runtime-profiles-draft.md


1. Crate Structure

Cargo workspace with 7 crates. Dependency arrows point downward.

conman (binary)
├── conman-api        (Axum router, handlers, middleware, extractors)
│   ├── conman-core
│   ├── conman-db
│   ├── conman-git
│   ├── conman-jobs
│   └── conman-auth
├── conman-core       (domain types, state machines, business rules — zero infra deps)
├── conman-db         (MongoDB repositories, index setup)
│   └── conman-core
├── conman-git        (Tonic client wrapping gitaly-rs gRPC)
│   └── conman-core
├── conman-jobs       (async job runner, workers)
│   ├── conman-core
│   ├── conman-db
│   └── conman-git
└── conman-auth       (password hashing, JWT, RBAC policy)
    └── conman-core

Crate responsibilities

conman-core — Pure domain layer. No IO, no frameworks. Contains:

conman-db — MongoDB persistence. Contains:

conman-git — Gitaly-rs gRPC client. Contains:

conman-auth — Authentication and authorization. Contains:

conman-api — HTTP layer. Contains:

conman-jobs — Background processing. Contains:

conman — Binary. Contains:


2. Conventions

Error handling

conman-core defines the error enum:

#[derive(Debug, thiserror::Error)]
pub enum ConmanError {
    #[error("not found: {entity} {id}")]
    NotFound { entity: &'static str, id: String },

    #[error("conflict: {message}")]
    Conflict { message: String },

    #[error("forbidden: {message}")]
    Forbidden { message: String },

    #[error("validation: {message}")]
    Validation { message: String },

    #[error("invalid state transition: {from} -> {to}")]
    InvalidTransition { from: String, to: String },

    #[error("git error: {message}")]
    Git { message: String },

    #[error("internal: {message}")]
    Internal { message: String },
}

conman-api maps this to HTTP responses:

impl IntoResponse for ConmanError {
    fn into_response(self) -> Response {
        let (status, code) = match &self {
            ConmanError::NotFound { .. } => (StatusCode::NOT_FOUND, "not_found"),
            ConmanError::Conflict { .. } => (StatusCode::CONFLICT, "conflict"),
            ConmanError::Forbidden { .. } => (StatusCode::FORBIDDEN, "forbidden"),
            ConmanError::Validation { .. } => (StatusCode::BAD_REQUEST, "validation_error"),
            ConmanError::InvalidTransition { .. } => (StatusCode::CONFLICT, "invalid_transition"),
            ConmanError::Git { .. } => (StatusCode::BAD_GATEWAY, "git_error"),
            ConmanError::Internal { .. } => (StatusCode::INTERNAL_SERVER_ERROR, "internal"),
        };
        // ... build JSON envelope
    }
}

Response envelope

Success:

{
  "data": { ... },
  "pagination": { "page": 1, "limit": 20, "total": 42 }
}

Error:

{
  "error": {
    "code": "not_found",
    "message": "not found: changeset abc123",
    "request_id": "req-uuid-here"
  }
}

Pagination

Query params page (1-based, default 1) and limit (default 20, max 100). Axum extractor:

#[derive(Debug, Deserialize)]
pub struct Pagination {
    #[serde(default = "default_page")]
    pub page: u64,
    #[serde(default = "default_limit")]
    pub limit: u64,
}

Request tracing

Every request gets a UUID via middleware (X-Request-Id header or generated). Conman standard is UUIDv7 for generated IDs. Propagated through tracing spans and included in error responses.

MongoDB patterns

File paths in API

File paths are always sent as query parameters or JSON body fields. Never as URL path segments (avoids encoding issues with / in paths).

Testing strategy

Audit pattern

Every mutation handler emits an audit event after success:

audit_repo.emit(AuditEvent {
    occurred_at: Utc::now(),
    actor_user_id: auth_user.id,
    app_id: Some(app_id),
    entity_type: "changeset",
    entity_id: changeset_id.to_hex(),
    action: "submitted",
    before: Some(serde_json::to_value(&old_state)?),
    after: Some(serde_json::to_value(&new_state)?),
    git_sha: Some(head_sha),
    context: request_context.clone(),
}).await;

Audit writes are fire-and-forget (logged on failure, never block the request).


3. Cross-Cutting Concerns

Authentication flow

  1. POST /api/auth/login — validate email/password → issue JWT (24h expiry)
  2. Axum middleware extracts Authorization: Bearer <token> header
  3. Middleware decodes JWT → queries app_memberships → populates Extension<AuthUser>:
    pub struct AuthUser {
        pub user_id: ObjectId,
        pub email: String,
        pub roles: HashMap<ObjectId, Role>,  // app_id -> role
    }
  4. Route handlers call auth_user.require_role(app_id, Role::ConfigManager)?
  5. Returns ConmanError::Forbidden on failure

Gitaly-rs connection

Configuration

Loaded from environment variables with CONMAN_ prefix:

Variable Default Description
CONMAN_PORT 3000 HTTP listen port
CONMAN_MONGO_URI mongodb://localhost:27017 MongoDB connection string
CONMAN_MONGO_DB conman Database name
CONMAN_GITALY_ADDRESS http://localhost:8075 gitaly-rs gRPC address
CONMAN_JWT_SECRET (required) JWT signing secret
CONMAN_JWT_EXPIRY_HOURS 24 JWT token lifetime
CONMAN_INVITE_EXPIRY_DAYS 7 Invite token lifetime
CONMAN_SECRETS_MASTER_KEY (required) Master key for envelope encryption of runtime secrets
CONMAN_TEMP_URL_DOMAIN (required) Base domain for generated temp runtime URLs

4. Domain Quick Reference

Terminology

Term Definition
App A managed config repository (1 app = 1 Git repo)
Workspace User-owned mutable branch (ws/<user>/<app>)
Changeset Reviewable proposal: workspace HEAD vs integration baseline
Release Immutable Git tag (rYYYY.MM.DD.N) of composed changesets
Environment Deploy target stage (Dev, QA, UAT, Prod)
Runtime Profile Versioned runtime blueprint (URL, env vars, secrets, DB/data strategy)
Canonical env Production-facing environment for baseline calculations
Baseline The reference point workspaces branch from (integration branch HEAD or canonical env release)

Changeset states

draft
  → submitted
    → in_review
      → approved → queued → released (terminal)
      → changes_requested → draft
      → rejected (terminal)
    queued → conflicted → draft
         → needs_revalidation → draft

Rules:

Release states

draft_release → assembling → validated → published
  → deployed_partial → deployed_full
  → rolled_back

Deployment states

pending → running → succeeded | failed | canceled

RBAC permission matrix

Capability user reviewer config_manager app_admin
Read app/repo metadata Y Y Y Y
Create/edit own workspace Y Y Y Y
Create/modify own changeset Y Y Y Y
Submit changeset Y Y Y Y
Comment in review Y Y Y Y
Approve/request changes/reject - Y Y Y
Move conflicted/needs_revalidation to draft Own Own Any Any
Assemble release from queue - - Y Y
Publish release - - Y Y
Deploy/promote release - - Y Y
Skip stage / concurrent deploy approval - Y Y Y
Invite users - - - Y
Manage app settings/roles/envs - - - Y

app_admin inherits all config_manager capabilities.

Baseline resolution

fn resolve_baseline(app: &App, envs: &[Environment], releases: &[Release]) -> String {
    match app.baseline_mode {
        BaselineMode::IntegrationHead => {
            format!("refs/heads/{}", app.integration_branch)
        }
        BaselineMode::CanonicalEnvRelease => {
            // Find latest deployed release to canonical environment
            // Fallback to integration branch HEAD if no release exists
        }
    }
}

Runtime profile defaults

pub struct ValidationGates {
    // submit: temp profile only
    pub submit_scope: ValidationScope, // TempOnly
    // release publish: environment profiles only
    pub release_scope: ValidationScope, // EnvOnly
    // deploy: target environment profile only
    pub deploy_scope: ValidationScope, // TargetEnvOnly
}

Runtime profile rules in v1:


5. Epic Index

Execution order is topological. Each epic file is self-contained with Rust types, database schemas, API endpoints, proto definitions, implementation checklist, and test cases.

Epic Name Dependencies Summary
E00 Platform Foundation none Server skeleton, MongoDB bootstrap, config, error envelope, pagination
E01 Git Adapter E00 Tonic client wrapping gitaly-rs gRPC services
E02 Auth & RBAC E00 Local auth, invites, memberships, role-based access
E03 App Setup E01, E02 App CRUD, settings, environment metadata, runtime profiles
E04 Workspaces E01, E03 Workspace lifecycle, file operations, guardrails
E05 Changesets E02, E04 Changeset lifecycle, review, comments, diffs, profile overrides
E06 Async Jobs E00, E05 Job framework, msuite workers, profile-aware gates/drift jobs
E07 Queue Orchestration E05, E06 Queue-first workflow, revalidation loop, override-key conflicts
E08 Releases E01, E06, E07 Release assembly, env-profile validation, tagging, publish
E09 Deployments E03, E06, E08 Deploy, promote, skip-stage, rollback, drift blocking
E10 Temp Environments E03, E06 On-demand envs, profile derivation, TTL, cleanup
E11 Notifications & Audit E05-E10 Email notifications, audit completeness, runtime profile events
E12 Hardening E08-E11 Load testing, fault injection, encryption/rotation runbooks

Critical path

E00 → E01 → E03 → E04 → E05 → E06 → E07 → E08 → E09

Parallelizable after E06: E08 can proceed with E10. E11 can run alongside E09/E10.

Milestone mapping

Milestone Epics Exit criteria
M1: Authoring + Review E00–E06 Users can author, submit, and review changesets
M2: Queue + Release E07–E08 Config managers can publish subset-based releases
M3: Environments + Recovery E09–E10 Full release movement and recovery paths
M4: Operations + Launch E11–E12 Production-readiness checklist passes