Viber runs your generated code in live Daytona sandboxes, providing instant preview with hot module replacement (HMR) and automatic dependency installation.
Sandbox architecture
Each Viber session creates an isolated Daytona workspace running a Vite development server:
┌─────────────────────────────────────────┐
│ Viber Frontend (Browser) │
│ ┌─────────────────────────────────┐ │
│ │ Preview iframe │ │
│ │ https://sandbox-id.daytona.app │ │
│ └─────────────────────────────────┘ │
│ ▲ │
│ │ WebSocket (HMR) │
└──────────────┼──────────────────────────┘
│
┌──────────────┼──────────────────────────┐
│ Daytona Sandbox │
│ │ │
│ ┌───────────▼──────────────┐ │
│ │ Vite Dev Server :5173 │ │
│ │ Hot Module Replacement │ │
│ └───────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌───────────┼──────────────┐ │
│ │ File System │ │
│ │ /projects/viber/src/ │ │
│ │ - App.tsx │ │
│ │ - components/ │ │
│ │ - Header.tsx │ │
│ │ - Hero.tsx │ │
│ └───────────────────────────┘ │
└─────────────────────────────────────────┘
Sandbox lifecycle
Create sandbox
When Viber loads, it creates a new Daytona workspace: src/lib/sandbox/service.ts
export async function createNewSandbox () : Promise < CreateSandboxResult > {
// Terminate any existing sandboxes
await sandboxManager . terminateAll ();
// Create new sandbox
const sandbox = createSandbox ();
const info : SandboxInfo = await sandbox . create ();
// Setup Vite app template
await sandbox . setupApp ();
// Register for management
sandboxManager . register ( info . sandboxId , sandbox );
return {
success: true ,
sandboxId: info . sandboxId ,
url: info . url , // e.g., https://abc123.daytona.app
};
}
Setup application
The sandbox initializes a React + Vite + TypeScript template with Tailwind CSS v4.
Start dev server
Vite dev server starts on port 5173 with HMR enabled.
Stream preview
Frontend embeds the sandbox URL in an iframe for live preview.
Apply code changes
As code is generated, files are written to the sandbox and HMR updates the preview instantly.
Sandbox manager
Viber uses a singleton manager to track active sandboxes:
src/lib/sandbox/manager.ts
class SandboxManager {
private sandboxes : Map < string , ManagedSandbox > = new Map ();
private activeSandboxId : string | null = null ;
register ( sandboxId : string , sandbox : DaytonaSandbox ) : void {
this . sandboxes . set ( sandboxId , {
sandboxId ,
sandbox ,
createdAt: new Date (),
lastAccessed: new Date (),
});
this . activeSandboxId = sandboxId ;
}
getActive () : DaytonaSandbox | null {
if ( ! this . activeSandboxId ) return null ;
const managed = this . sandboxes . get ( this . activeSandboxId );
if ( managed ) {
managed . lastAccessed = new Date ();
return managed . sandbox ;
}
return null ;
}
async terminate ( sandboxId : string ) : Promise < void > {
const managed = this . sandboxes . get ( sandboxId );
if ( managed ) {
await managed . sandbox . destroy ();
this . sandboxes . delete ( sandboxId );
}
}
}
export const sandboxManager = new SandboxManager ();
The sandbox manager ensures only one active sandbox per session to conserve resources.
File operations
Writing files
Code is applied to the sandbox through the file API:
src/lib/sandbox/service.ts
export async function applyFilesToSandbox (
files : SandboxFile [],
sandboxId ?: string ,
onProgress ?: ( current : number , total : number , fileName : string ) => void
) : Promise <{ success : boolean ; appliedFiles : string [] }> {
const sandbox = sandboxId
? sandboxManager . get ( sandboxId )
: sandboxManager . getActive ();
if ( ! sandbox ) {
return { success: false , appliedFiles: [], error: "No active sandbox" };
}
const appliedFiles : string [] = [];
await Promise . all (
files . map ( async ( file , index ) => {
onProgress ?.( index + 1 , files . length , file . path );
await sandbox . write ( file . path , file . content );
appliedFiles . push ( file . path );
})
);
return { success: true , appliedFiles };
}
Reading files
For edit mode, Viber reads existing files to provide context:
src/lib/sandbox/service.ts
export async function getSandboxFileContents (
filePaths : string [],
sandboxId ?: string
) : Promise < SandboxFilesResult > {
const sandbox = sandboxId
? sandboxManager . get ( sandboxId )
: sandboxManager . getActive ();
if ( ! sandbox ) {
return { success: false , error: "No active sandbox" };
}
const fileContents : Record < string , string > = {};
await Promise . all (
filePaths . map ( async ( file ) => {
try {
fileContents [ file ] = await sandbox . read ( file );
} catch {
// Skip unreadable files
}
})
);
return { success: true , files: fileContents };
}
Preview iframe
The preview is rendered in a sandboxed iframe:
src/components/builder/preview/sandbox-iframe.tsx
export function SandboxIframe ({ url , refreshKey = 0 } : SandboxIframeProps ) {
// Add cache-busting parameter to force fresh load
const cacheBustedUrl = ` ${ url }${ url . includes ( "?" ) ? "&" : "?" } _t= ${ refreshKey } ` ;
return (
< iframe
key = { refreshKey }
src = { cacheBustedUrl }
className = "w-full h-full border-0 bg-white"
title = "Preview"
allow = "cross-origin-isolated"
sandbox = "allow-scripts allow-same-origin allow-forms allow-popups allow-modals"
/>
);
}
The iframe includes sandbox attributes for security, allowing scripts and forms while isolating from the parent page.
Package installation
When the AI generates <package> tags, Viber automatically installs them:
src/lib/sandbox/service.ts
export async function installPackages (
packages : string [],
sandboxId ?: string
) : Promise <{ success : boolean ; error ?: string }> {
const sandbox = sandboxId
? sandboxManager . get ( sandboxId )
: sandboxManager . getActive ();
if ( ! sandbox ) {
return { success: false , error: "No active sandbox" };
}
// Runs npm install <packages> in the sandbox
const result = await sandbox . install ( packages );
return { success: result . success , error: result . stderr || undefined };
}
Auto-install flow
Pre-installed packages
AI generates code with <package>lucide-react</package>
Parser extracts package name
Backend runs npm install lucide-react in sandbox
Vite HMR updates preview with new package
The Vite template includes:
react and react-dom
typescript
tailwindcss v4
lucide-react (for icons)
vite and related plugins
Hot module replacement
Vite’s HMR provides instant updates without full page reloads:
// When you write a file:
await sandbox . write ( "src/components/Hero.tsx" , content );
// Vite detects the change
// → Recompiles Hero.tsx
// → Sends HMR update over WebSocket
// → React Fast Refresh updates component
// → Preview updates in <200ms
HMR preserves component state during updates, so you don’t lose form inputs or scroll position.
Sandbox status
Viber monitors sandbox health:
src/lib/sandbox/service.ts
export async function getSandboxStatus (
sandboxId ?: string
) : Promise < SandboxStatusResult > {
const id = sandboxId || sandboxManager . getActiveSandboxId ();
if ( ! id ) {
return { success: false , error: "No active sandbox" };
}
const sandbox = sandboxManager . get ( id );
if ( ! sandbox ) {
return { success: false , error: "Sandbox not found" };
}
const info = sandbox . getInfo ();
return {
success: true ,
isAlive: sandbox . isActive (),
sandboxId: id ,
url: info ?. url ,
};
}
This powers the “Setting up workspace…” status in the UI.
Diagnostics
Viber can run build checks in the sandbox:
src/lib/sandbox/service.ts
export async function runSandboxDiagnostics (
sandboxId ?: string
) : Promise <{ success : boolean ; output ?: string }> {
const sandbox = sandboxId
? sandboxManager . get ( sandboxId )
: sandboxManager . getActive ();
if ( ! sandbox ) {
return { success: false , error: "No active sandbox" };
}
// Runs `npm run build` or similar
const result = await sandbox . runDiagnostics ();
return {
success: result . success ,
output: result . output ,
};
}
This helps catch TypeScript errors or build issues.
Restarting the dev server
If the preview becomes unresponsive, restart the dev server:
src/lib/sandbox/service.ts
export async function restartSandbox (
sandboxId ?: string
) : Promise <{ success : boolean ; error ?: string }> {
const sandbox = sandboxId
? sandboxManager . get ( sandboxId )
: sandboxManager . getActive ();
if ( ! sandbox ) {
return { success: false , error: "No active sandbox" };
}
await sandbox . restartDevServer ();
return { success: true };
}
Sandbox cleanup
Manual termination await sandboxManager . terminate ( sandboxId );
Destroys a specific sandbox.
Automatic cleanup await sandboxManager . cleanup ( maxAge );
Removes sandboxes older than maxAge milliseconds.
Terminate all await sandboxManager . terminateAll ();
Destroys all managed sandboxes.
Session isolation Each browser session gets its own sandbox, isolated from others.
The preview panel includes controls for interacting with the sandbox:
< PreviewToolbar >
< Button onClick = { handleRefresh } > Refresh </ Button >
< Button onClick = { handleOpenInNewTab } > Open in new tab </ Button >
< Button onClick = { handleRestart } > Restart server </ Button >
</ PreviewToolbar >
Apply multiple files in parallel for faster updates: await Promise . all (
files . map ( file => sandbox . write ( file . path , file . content ))
);
HMR only rebuilds changed modules, not the entire app.
Each sandbox has CPU and memory limits. Viber terminates old sandboxes to free resources.
File writes are async. UI shows “Applying changes…” during writes.
Configuration
Sandbox behavior is configured through environment variables:
# Daytona API endpoint
DAYTONA_API_URL = https://api.daytona.io
# Authentication
DAYTONA_API_KEY = your_api_key
# Default workspace template
DEFAULT_TEMPLATE = react-vite-ts
Viber requires a Daytona account with API access. Contact Daytona to get an API key.