This document defines the product model for JJHub cloud workspaces, preview environments, and sandboxed execution. It covers the end-user experience across CLI, API, and web UI. Implementation details of the underlying VM platform are outside the scope of this spec.
JJHub workspaces are cloud development environments attached to repositories. Every bookmark in a repository can have an associated workspace. Workspaces are created lazily on first access and hibernate automatically when idle. They preserve full memory state across sessions so developers resume exactly where they left off.
Workspaces serve three product purposes:
- interactive cloud development with terminal access
- preview environments for Landing Requests
- sandboxed execution for workflows, CI, and AI agents
All three use the same underlying workspace primitive. There is no separate "runner" or "sandbox" concept visible to users.
A workspace is a persistent cloud environment scoped to a repository bookmark and a user. Each workspace contains:
- a full clone of the repository at the bookmark's head
- a Linux environment with the tools defined by the repository's workspace template
- any running services (dev servers, databases, etc.)
- the user's session state (open files, terminal history, running processes)
Workspaces have five states:
- starting — environment is being provisioned
- running — actively executing, consuming compute
- suspended — hibernated with full memory state preserved, storage cost only
- stopped — shut down, disk preserved but memory state lost
- failed — provisioning or execution error
A workspace template defines the base environment for a repository's workspaces. Templates are defined in .jjhub/workspace.ts and committed to the repository.
import { defineWorkspace } from "@jjhub-ai/workflow";
export default defineWorkspace({
// Tools and runtimes to install (cached in base layer)
tools: {
bun: "latest",
jj: "latest",
},
// System packages
packages: ["curl", "git", "jq", "build-essential"],
// Install command (runs once after clone, cached in snapshot)
install: "bun install",
// Services to run in the workspace
services: {
"dev-server": {
command: "bun run dev",
port: 3000,
},
database: {
command: "postgres -D /var/lib/postgresql/data",
port: 5432,
},
},
// Environment variables (non-secret, committed to repo)
env: {
NODE_ENV: "development",
DATABASE_URL: "postgresql://localhost:5432/dev",
},
// Linux user configuration
user: "developer",
});Templates are cached as layered snapshots. The base layer (tools, packages, install) is built once and reused across all workspaces for the repository. The outer layer (repo checkout at specific revision, user config, secrets) is applied per workspace.
When no .jjhub/workspace.ts exists, JJHub uses a default template with Bun, jj, and git.
A preview environment is a workspace that is automatically created for every Landing Request. It runs the repository's preview configuration and exposes the result on a public URL.
Preview configuration is defined in .jjhub/preview.ts:
import { definePreview } from "@jjhub-ai/workflow";
export default definePreview({
// Port to expose as the preview URL
port: 3000,
// Install and start commands
install: "bun install",
start: "bun run dev",
// Environment variables for preview
env: {
NODE_ENV: "preview",
},
// Optional: full control via setup function
setup: async (workspace) => {
await workspace.exec("bun install");
await workspace.exec("bun run db:seed");
},
// Optional: additional services
services: {
database: {
command: "postgres -D /var/lib/postgresql/data",
port: 5432,
},
},
});When no .jjhub/preview.ts exists but .jjhub/workspace.ts defines a service with a port, that service is used as the preview.
Preview environments:
- are created automatically when a Landing Request is opened
- are suspended when the Landing Request is idle
- resume automatically on access (visiting the preview URL)
- are deleted when the Landing Request is landed or closed
- use ephemeral persistence (no long-term storage cost)
Preview URLs follow the pattern {lr-number}-{repo}.preview.jjhub.tech.
Workspaces support collaborative access with permission levels:
| Permission | Capabilities |
|---|---|
| owner | Full control, manage sharing, delete |
| editor | Terminal access, file read/write, service management |
| viewer | Read-only file access, view service logs, view terminal replay |
Sharing options:
- share with specific users (by username)
- share with all repository collaborators
- share with anyone who has the link (viewer only)
Each collaborator gets a separate Linux user and SSH identity scoped to their permission level. Multiple users can access the same workspace concurrently.
A snapshot captures the complete state of a workspace (memory, disk, running processes) as an immutable image. Snapshots can be:
- created manually from a running workspace
- used to create new workspaces instantly
- shared with teammates as reusable environment templates
jjhub workspace create [--repo OWNER/REPO]
--bookmark <name> Bookmark to attach to (default: current)
--snapshot <id> Create from snapshot
--template <name> Use named template
--persistence <type> ephemeral | sticky | persistent (default: sticky)
--idle-timeout <seconds> Auto-suspend timeout (default: 1800)
--no-idle-timeout Never auto-suspend
jjhub workspace list [--repo OWNER/REPO]
--state <filter> running | suspended | stopped | all (default: all)
--bookmark <name> Filter by bookmark
jjhub workspace view <id> [--repo OWNER/REPO]
Shows: status, bookmark, uptime, services, preview URL, sharing, persistence
jjhub workspace delete <id> [--repo OWNER/REPO]
jjhub workspace suspend <id> [--repo OWNER/REPO]
Hibernate with full state preservation. Resume is instant.
jjhub workspace resume <id> [--repo OWNER/REPO]
Resume from suspended state. Sub-second.jjhub workspace ssh [id] [--repo OWNER/REPO]
--bookmark <name> SSH into workspace for this bookmark (default: current)
--user <username> SSH as specific Linux user
--print-command Print SSH command instead of connecting
If no workspace exists for the bookmark, one is created automatically.
If the workspace is suspended, it resumes automatically on connect.
jjhub workspace exec <id> <command> [--repo OWNER/REPO]
--user <username> Run as specific user
--timeout <seconds> Execution timeout (default: 120)
jjhub workspace run <id> [--repo OWNER/REPO]
--code <typescript> TypeScript code to execute
--file <path> Local .ts file to execute in workspace
--workdir <dir> Working directory inside workspace
jjhub workspace files <id> ls <path> [--repo OWNER/REPO]
jjhub workspace files <id> cat <path> [--repo OWNER/REPO]
jjhub workspace files <id> write <path> [--repo OWNER/REPO] [--stdin]
jjhub workspace files <id> watch [--repo OWNER/REPO]
Stream file change notifications in real-time.
jjhub workspace services <id> [--repo OWNER/REPO]
List all services with status (running, stopped, failed).
jjhub workspace logs <id> [--repo OWNER/REPO]
--service <name> Specific service (default: all)
--follow Stream logs in real-time
--lines <n> Number of historical lines (default: 100)
jjhub workspace restart <id> <service> [--repo OWNER/REPO]
jjhub workspace fork <id> [--repo OWNER/REPO]
--name <name> Name for the forked workspace
Copy-on-write clone of a running workspace. Original is not paused.
jjhub workspace snapshot create <id> [--repo OWNER/REPO]
--name <name> Name for the snapshot
jjhub workspace snapshot list [--repo OWNER/REPO]
jjhub workspace snapshot delete <id> [--repo OWNER/REPO]
jjhub workspace share <id> [--repo OWNER/REPO]
--user <username> Share with specific user
--team <team> Share with team
--collaborators Share with all repo collaborators
--permission <level> owner | editor | viewer (default: editor)
jjhub workspace unshare <id> [--repo OWNER/REPO]
--user <username> Revoke specific user
--team <team> Revoke team
--all Revoke all sharing
jjhub workspace preview <lr-number> [--repo OWNER/REPO]
Show the preview URL for a Landing Request.
If the preview is suspended, it resumes automatically.
jjhub workspace preview logs <lr-number> [--repo OWNER/REPO]
--follow Stream preview service logs
Preview URLs are also displayed in:
jjhub lr view <number>outputjjhub lr listoutput (when a preview exists)
jjhub workspace watch <id> [--repo OWNER/REPO]
Stream workspace state changes in real-time.
Shows: provisioning progress, service startup, state transitions.
All workspace operations go through the JJHub API. Clients never interact with the underlying VM platform directly.
POST /api/repos/{owner}/{repo}/workspaces
GET /api/repos/{owner}/{repo}/workspaces
GET /api/repos/{owner}/{repo}/workspaces/{id}
DELETE /api/repos/{owner}/{repo}/workspaces/{id}
POST /api/repos/{owner}/{repo}/workspaces/{id}/suspend
POST /api/repos/{owner}/{repo}/workspaces/{id}/resume
GET /api/repos/{owner}/{repo}/workspaces/{id}/ssh
POST /api/repos/{owner}/{repo}/workspaces/{id}/exec
POST /api/repos/{owner}/{repo}/workspaces/{id}/run
GET /api/repos/{owner}/{repo}/workspaces/{id}/terminal (WebSocket)
GET /api/repos/{owner}/{repo}/workspaces/{id}/files/{path}
PUT /api/repos/{owner}/{repo}/workspaces/{id}/files/{path}
GET /api/repos/{owner}/{repo}/workspaces/{id}/files (list directory)
DELETE /api/repos/{owner}/{repo}/workspaces/{id}/files/{path}
GET /api/repos/{owner}/{repo}/workspaces/{id}/files/watch (SSE)
GET /api/repos/{owner}/{repo}/workspaces/{id}/services
GET /api/repos/{owner}/{repo}/workspaces/{id}/services/{name}/logs
POST /api/repos/{owner}/{repo}/workspaces/{id}/services/{name}/restart
GET /api/repos/{owner}/{repo}/workspaces/{id}/services/{name}/status
POST /api/repos/{owner}/{repo}/workspaces/{id}/fork
POST /api/repos/{owner}/{repo}/workspaces/{id}/snapshot
GET /api/repos/{owner}/{repo}/workspace-snapshots
GET /api/repos/{owner}/{repo}/workspace-snapshots/{id}
DELETE /api/repos/{owner}/{repo}/workspace-snapshots/{id}
GET /api/repos/{owner}/{repo}/workspaces/{id}/shares
POST /api/repos/{owner}/{repo}/workspaces/{id}/shares
DELETE /api/repos/{owner}/{repo}/workspaces/{id}/shares/{share-id}
PATCH /api/repos/{owner}/{repo}/workspaces/{id}/shares/{share-id}
GET /api/repos/{owner}/{repo}/landings/{number}/preview
POST /api/repos/{owner}/{repo}/landings/{number}/preview (manual trigger)
DELETE /api/repos/{owner}/{repo}/landings/{number}/preview
GET /api/repos/{owner}/{repo}/landings/{number}/preview/logs (SSE)
GET /api/repos/{owner}/{repo}/workspaces/{id}/stream (SSE)
Workspace state changes, service status, provisioning progress.
GET /api/repos/{owner}/{repo}/workspaces/{id}/services/{name}/logs?follow=true (SSE)
Real-time service log streaming.
The repository page includes a "Workspaces" tab showing:
- all workspaces for the repository, grouped by bookmark
- status badge (running, suspended, stopped)
- owner avatar
- last activity time
- quick actions: SSH, suspend, resume, delete
The workspace detail page shows:
- status and uptime
- bookmark and revision
- services list with status and logs
- terminal access (embedded xterm)
- file browser
- sharing controls
- snapshot history
- preview URL (if attached to a Landing Request)
The Landing Request detail page includes:
- a "Preview" tab with an embedded iframe showing the preview environment
- a "Open Preview" button that opens the preview URL in a new tab
- preview service logs
- preview status badge (starting, running, suspended)
- the preview resumes automatically when the tab is viewed
The terminal view provides:
- full xterm terminal connected via WebSocket to SSH
- terminal resize support
- connection status indicator
- workspace state in the header (running, services status)
When viewing an issue that has an active workflow run:
- the issue page shows the workspace where the agent is working
- live streaming of agent output
- link to the workspace terminal for manual intervention
- link to the resulting Landing Request when the workflow completes
Workspaces are not created eagerly for every bookmark. They are created on first access:
- user runs
jjhub workspace ssh --bookmark feature-x - user opens a workspace from the UI for a specific bookmark
- a workflow or agent task requires a workspace for a bookmark
- a Landing Request triggers a preview environment
Each user can have at most one active workspace per bookmark in a repository. "Active" means any state except stopped. This constraint is enforced by the database.
If a user already has a workspace for a bookmark, subsequent access reuses it:
- if running, connect directly
- if suspended, resume automatically then connect
- if starting, wait for it to finish then connect
When the bookmark advances (new commits are pushed), the workspace does not automatically update. The user must explicitly pull changes within their workspace. This matches the mental model of a local checkout — your workspace is your working copy.
Workflow steps that need code execution run inside workspaces. The workflow runtime creates ephemeral workspaces from cached snapshots, executes steps, and tears them down.
Workflow-created workspaces:
- use ephemeral persistence (auto-deleted after use)
- are not visible in the user's workspace list
- use the repository's workspace template for consistent environments
- have secrets injected via environment variables (never written to disk)
The automated issue pipeline (Research, Plan, Implement, Review) uses workspaces at the Implement step:
- Workflow creates an ephemeral workspace from the repository's cached snapshot
- Agent runs inside the workspace with the plan artifact as input
- Agent writes code, runs tests, commits with jj
- Workflow collects change IDs and creates a Landing Request
- Workspace is deleted
The user can observe this in real-time via the issue page, the workflow run page, or the CLI (jjhub workflow watch).
CI steps (lint, test, build, e2e) run in ephemeral workspaces:
- created from the repository's cached snapshot (sub-second startup)
- each step can run in parallel in separate workspaces
- results reported as commit statuses on the Landing Request
- workspaces deleted after completion
- logs and artifacts preserved in the workflow run record
Each workspace is a fully isolated virtual machine. Workspaces for different users and repositories share no state, filesystem, or network namespace.
- Secrets are injected as environment variables at runtime, never written to the filesystem
- Repository secrets (configured via
jjhub secret set) are available in workflow workspaces - User credentials (Claude auth, API keys) are injected per-session and scoped to the workspace
- SSH tokens are temporary, per-session, and scoped to a specific workspace and Linux user
Workspace access requires:
- authentication with JJHub
- repository read permission (minimum) for viewing
- repository write permission for creating, executing, and modifying
- explicit sharing grant for accessing another user's workspace
Workspaces have outbound internet access for package installation and API calls. Inbound access is limited to:
- SSH via the JJHub SSH proxy
- preview URL traffic via the JJHub preview proxy
- no direct network access between workspaces
Workspace usage is metered against the user's or organization's subscription plan:
| Resource | Metering |
|---|---|
| Workspace compute hours | Time in running state, per-minute billing |
| Workspace storage | Time in suspended state, per-GB/hour |
| Preview environments | Count against workspace compute hours |
| Concurrent workspaces | Plan limit (e.g., 5 concurrent for Pro) |
| Snapshots | Count and storage against plan limits |
Suspended workspaces cost significantly less than running workspaces. Users are encouraged to let idle timeouts suspend workspaces rather than keeping them running.
| Term | Definition |
|---|---|
| Workspace | A cloud development environment attached to a repository bookmark |
| Preview | A workspace automatically created for a Landing Request with an exposed URL |
| Snapshot | An immutable capture of a workspace's complete state |
| Template | A repository-committed configuration defining the workspace environment |
| Fork | A copy-on-write clone of a running workspace |
| Suspend | Hibernate a workspace with full memory state preservation |
| Resume | Wake a suspended workspace, restoring exact previous state |
The workspace platform is an internal concern. User-facing surfaces (CLI, API, UI, documentation, error messages) must not reference the underlying VM provider. The product vocabulary is "workspace," "preview," "snapshot," "suspend," and "resume" — not VM, sandbox, container, or provider-specific terms.