04 — Deploy Pipeline

The deploy pipeline handles building, versioning, swapping, and rolling back application deployments with zero downtime.

Current State

bext-server has basic deployment primitives:

  • POST /api/build — run build script, reload JSC pool
  • POST /api/reload — read bundle from disk, swap JSC pool
  • kill -HUP <pid> — same as /api/reload
  • File watcher — auto-rebuild on source change (dev mode)
  • Atomic swap: new JSC pool created first, old pool replaced only on success

Target State

A full deploy pipeline:

bext deploy ./my-app --app marketing
  1. Validate source directory
  2. Detect framework, scan routes
  3. Run build (transforms + bundler)
  4. Create versioned artifact
  5. Create new isolate with new bundle
  6. Health check new isolate
  7. Atomic swap: old → new
  8. Clear app's ISR cache
  9. Record deployment in registry
  10. Drain old isolate after grace period

Build Process

Build Steps

Source directory
  │
  ├── 1. Framework detection (RT-1)
  │   └── Determines build strategy
  │
  ├── 2. Route scan
  │   └── Discover pages, API routes, static params
  │   └── Detect PPR, "use cache", metadata exports
  │
  ├── 3. Transform pipeline
  │   ├── import_strip (remove server-only packages)
  │   ├── barrel_optimize (tree-shake barrel exports)
  │   ├── font_optimize (replace next/font with static objects)
  │   ├── env_inline (inject NEXT_PUBLIC_* vars)
  │   ├── cache_directive ("use cache" → __bextCache wrapper)
  │   ├── server_boundary ("use server"/"use client" boundaries)
  │   └── ... (remaining transforms per profile)
  │
  ├── 4. Bundle (framework-specific)
  │   ├── Next.js: Bun build → SSR bundle + client chunks
  │   ├── Hono: esbuild → single JS file
  │   ├── Static: copy files to output
  │   └── Custom: run user's build script
  │
  ├── 5. Static generation (if applicable)
  │   └── Pre-render pages with generateStaticParams
  │   └── Output: static HTML files + static-paths.json manifest
  │
  ├── 6. Artifact creation
  │   └── Versioned directory: builds/{app_id}/{version}/
  │   └── Contains: bundle.js, static/, routes.json, manifest.json
  │
  └── 7. Artifact verification
      └── Load bundle in temp isolate, render a test page
      └── Verify routes match expectations

Build Config

[apps.marketing.build]
# Use bext's built-in bundler
bundler = "bun"                      # "bun", "esbuild", "custom"
entry = "src/app"                    # Entry point (framework-specific)
output = "builds/marketing"          # Output directory

# Custom build script (overrides bundler)
# script = "scripts/build.sh"

# Environment variables for build
[apps.marketing.build.env]
NODE_ENV = "production"
NEXT_PUBLIC_API_URL = "https://api.example.com"

# Static generation
[apps.marketing.build.static]
enabled = true                       # Pre-render static pages
concurrent = 4                       # Parallel static generation

Version Management

Versioning Strategy

Each deploy creates a version. Version identifiers:

1. Auto-increment:  v1, v2, v3, ...
2. Git SHA:         abc1234 (if git repo)
3. Timestamp:       20260328-153042
4. Custom:          --version "1.2.3"

Default: git SHA if available, else timestamp.

Version Storage

data/
  builds/
    marketing/
      abc1234/                    # Version directory
        bundle.js                 # SSR bundle
        static/                   # Static assets
        routes.json               # Scanned routes
        manifest.json             # Build metadata
      def5678/                    # Previous version (kept for rollback)
      ...

Manifest

{
  "app_id": "marketing",
  "version": "abc1234",
  "framework": "nextjs",
  "built_at": "2026-03-28T15:30:42Z",
  "built_by": "deploy",
  "bundle_size": 245760,
  "routes": {
    "pages": 42,
    "api": 8,
    "static": 12
  },
  "transforms_applied": ["import_strip", "env_inline", "font_optimize"],
  "build_duration_ms": 3421,
  "git_sha": "abc1234def5678...",
  "git_branch": "main"
}

Zero-Downtime Swap

Swap Protocol

Timeline:
  t0: New version built, artifact ready
  t1: Create new isolate with new bundle
  t2: Health check new isolate (render test page)
  t3: If healthy → atomic swap in AppRouter
  t4: New requests go to new isolate
  t5: Old isolate enters draining state
  t6: After drain_timeout (30s), old isolate shut down
  t7: Old version's ISR cache entries invalidated
  t8: Deploy recorded in registry
pub struct DeploySwap {
    pub app_id: String,
    pub old_version: Option,
    pub new_version: AppVersion,
    pub drain_timeout: Duration,       // default: 30s
    pub health_check_timeout: Duration, // default: 5s
}

impl DeploySwap {
    pub async fn execute(&self, manager: &IsolateManager, router: &AppRouter) -> Result {
        // 1. Create new isolate
        let new_isolate = manager.create(self.new_version.isolate_config())?;

        // 2. Health check
        let health = new_isolate.render("/", "{}").await;
        if health.is_err() {
            manager.evict(&new_isolate.id);
            return Err(DeployError::HealthCheckFailed(health.unwrap_err()));
        }

        // 3. Atomic swap
        router.swap_version(&self.app_id, &self.new_version);

        // 4. Drain old isolate
        if let Some(ref old) = self.old_version {
            tokio::spawn(async move {
                tokio::time::sleep(self.drain_timeout).await;
                manager.evict(&old.isolate_id);
            });
        }

        // 5. Invalidate old cache
        manager.invalidate_app_cache(&self.app_id);

        Ok(DeployResult::Success)
    }
}

Rollback

Instant rollback to the previous version:

bext rollback marketing

Flow:

  1. Look up previous version in registry
  2. Create isolate with previous bundle (or reuse if still alive)
  3. Atomic swap to previous version
  4. Invalidate ISR cache
  5. Mark current version as rolled back

Constraint: Only one rollback level kept by default. Configurable:

[apps.marketing]
keep_versions = 3    # Keep last 3 versions for rollback

Implementation Tasks

DP-1: Build System

Tasks:

  • Create bext-core/src/platform/build.rs
  • Framework-specific build strategies (Next.js, Hono, static)
  • Transform pipeline integration (use existing transforms)
  • Bun bundler integration (shell out to bun build)
  • esbuild integration (for lightweight apps)
  • Custom build script support
  • Static generation phase (pre-render static pages)
  • Artifact creation (versioned directory with manifest)
  • Artifact verification (load in temp isolate, render test)
  • Build output streaming (show progress in CLI)
  • Build caching (skip rebuild if source unchanged)

DP-2: Version Manager

Tasks:

  • Create bext-core/src/platform/versions.rs
  • Version naming strategy (git SHA, timestamp, custom)
  • SQLite storage for version records
  • create_version() — build + store artifact
  • set_current() — mark version as active
  • get_previous() — for rollback
  • prune() — delete old versions beyond keep_versions limit
  • Disk cleanup (delete old build artifacts)
  • Version comparison (diff routes between versions)

DP-3: Zero-Downtime Swap

Tasks:

  • Create bext-core/src/platform/deploy.rs
  • DeploySwap struct with the swap protocol
  • Health check: render a test page in new isolate
  • Atomic swap in AppRouter (DashMap replace)
  • Drain protocol: old isolate serves in-flight requests, then shuts down
  • ISR cache invalidation scoped to app
  • Compression cache invalidation scoped to app
  • Deploy event logging (who, when, version, duration)
  • Failure recovery: if swap fails, old version continues

DP-4: Rollback

Tasks:

  • bext rollback <app> command
  • Load previous version's bundle
  • Reuse existing isolate if still alive (within drain timeout)
  • If evicted, create new isolate from stored artifact
  • Swap + cache invalidation (same as deploy)
  • Record rollback event in deploy history
  • Test: deploy → rollback → deploy cycle

DP-5: Deploy Hooks

Extensible hooks for the deploy lifecycle:

[apps.marketing.hooks]
pre_build = "scripts/pre-build.sh"
post_build = "scripts/post-build.sh"
pre_deploy = "scripts/pre-deploy.sh"     # Can abort deploy
post_deploy = "scripts/post-deploy.sh"
on_rollback = "scripts/on-rollback.sh"

Also: WASM plugin hooks for deploy events:

// LifecyclePlugin trait extension
fn on_deploy(&self, app_id: &str, version: &str) -> Result<(), String>;
fn on_rollback(&self, app_id: &str, from: &str, to: &str) -> Result<(), String>;

Tasks:

  • Shell hook execution with timeout
  • Pre-deploy hook can abort (non-zero exit = cancel)
  • Environment variables passed to hooks (APP_ID, VERSION, etc.)
  • Extend LifecyclePlugin trait with deploy events
  • Fire deploy events to all registered lifecycle plugins

DP-6: Deploy API (HTTP)

REST API for triggering deploys programmatically (CI/CD integration):

POST /api/platform/deploy
  { "app_id": "marketing", "source": "./apps/marketing", "version": "abc1234" }

POST /api/platform/rollback
  { "app_id": "marketing" }

GET /api/platform/deploys?app=marketing&limit=10
  [{ "version": "abc1234", "status": "success", "deployed_at": "..." }, ...]

POST /api/platform/promote
  { "app_id": "marketing", "to": 100 }

Tasks:

  • Deploy endpoint (triggers full pipeline)
  • Rollback endpoint
  • Deploy history endpoint
  • Promote endpoint (canary traffic management)
  • Webhook notifications on deploy events
  • Auth: deploy operations require platform admin token

Performance Targets

Metric Target
Build (Next.js, warm cache) < 10s
Build (static site) < 2s
Swap latency (new → live) < 100ms
Rollback latency < 500ms
Zero dropped requests during swap 0
Deploy API response < 30s (streams progress)