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 poolPOST /api/reload— read bundle from disk, swap JSC poolkill -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:
- Look up previous version in registry
- Create isolate with previous bundle (or reuse if still alive)
- Atomic swap to previous version
- Invalidate ISR cache
- 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 -
DeploySwapstruct 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) |