Purpose: Fast sandbox creation from pre-built templates
How it works:
e2b template build → Creates template with snapshot → Stored as template
User creates project → Sandbox.create(template) → Restores from template snapshot
What it provides:
- Pre-installed dependencies (Node.js, npm packages, etc.)
- Template files ready at
/templates/{name}/ - Fast boot time (~200-500ms)
Purpose: Save and restore a specific sandbox's running state
How it works:
User working in sandbox → sandbox.takeSnapshot() → Creates runtime snapshot
User returns later → Sandbox.create({ snapshotId }) → Restores from runtime snapshot
What it provides:
- Preserve user's file changes
- Keep running processes (dev server still running!)
- Resume exactly where user left off
- No need to reinstall or restart
Create migration: src/lib/db/migrations/YYYYMMDD_add_project_snapshots.sql
-- Table for storing project runtime snapshots
CREATE TABLE IF NOT EXISTS project_snapshots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
snapshot_id TEXT NOT NULL,
description TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
file_count INTEGER,
size_mb INTEGER,
metadata JSONB DEFAULT '{}'::jsonb
);
-- Index for fast project lookups
CREATE INDEX idx_project_snapshots_project_id ON project_snapshots(project_id);
CREATE INDEX idx_project_snapshots_created_at ON project_snapshots(created_at DESC);
-- RLS policies
ALTER TABLE project_snapshots ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Users can view snapshots for their team's projects"
ON project_snapshots FOR SELECT
USING (
EXISTS (
SELECT 1 FROM projects p
WHERE p.id = project_snapshots.project_id
AND p.team_id IN (
SELECT team_id FROM users_teams WHERE user_id = auth.uid()
)
)
);
CREATE POLICY "Users can create snapshots for their team's projects"
ON project_snapshots FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM projects p
WHERE p.id = project_snapshots.project_id
AND p.team_id IN (
SELECT 1 FROM users_teams WHERE user_id = auth.uid()
)
)
);
CREATE POLICY "Users can delete snapshots for their team's projects"
ON project_snapshots FOR DELETE
USING (
EXISTS (
SELECT 1 FROM projects p
WHERE p.id = project_snapshots.project_id
AND p.team_id IN (
SELECT 1 FROM users_teams WHERE user_id = auth.uid()
)
)
);Create: src/lib/db/snapshots.ts
import { getDb, handleDbError } from './index';
export async function createProjectSnapshot(data: {
project_id: string;
snapshot_id: string;
description?: string;
file_count?: number;
size_mb?: number;
}) {
return handleDbError(async () => {
const db = await getDb();
const { data: snapshot, error } = await db
.from('project_snapshots')
.insert(data)
.select()
.single();
if (error) throw error;
return snapshot;
}, 'createProjectSnapshot');
}
export async function getProjectSnapshots(projectId: string) {
return handleDbError(async () => {
const db = await getDb();
const { data, error } = await db
.from('project_snapshots')
.select('*')
.eq('project_id', projectId)
.order('created_at', { ascending: false });
if (error) throw error;
return data || [];
}, 'getProjectSnapshots');
}
export async function getLatestSnapshot(projectId: string) {
return handleDbError(async () => {
const db = await getDb();
const { data, error } = await db
.from('project_snapshots')
.select('*')
.eq('project_id', projectId)
.order('created_at', { ascending: false })
.limit(1)
.maybeSingle();
if (error) throw error;
return data;
}, 'getLatestSnapshot');
}
export async function deleteProjectSnapshot(snapshotId: string) {
return handleDbError(async () => {
const db = await getDb();
const { error } = await db
.from('project_snapshots')
.delete()
.eq('snapshot_id', snapshotId);
if (error) throw error;
}, 'deleteProjectSnapshot');
}Add to src/lib/services/e2b-service.ts:
import { SnapshotService } from './snapshot-service';
export class E2BService {
// ... existing methods ...
/**
* Create sandbox from latest snapshot if available, otherwise from template
*/
static async getOrCreateSandboxWithSnapshot(projectId: string, supabase: any) {
const project = await getProject(projectId);
if (!project) {
throw new Error(`Project ${projectId} not found`);
}
const teamApiKey = await TeamApiKeyService.getTeamApiKey(project.team_id, supabase);
// Check for latest snapshot
const latestSnapshot = await SnapshotService.getLatestSnapshot(projectId);
if (latestSnapshot) {
console.log('[E2B] Restoring from snapshot:', latestSnapshot.snapshot_id);
try {
const sandbox = await SnapshotService.restoreFromSnapshot(
latestSnapshot.snapshot_id,
teamApiKey
);
// Save to database
const sandboxData = {
project_id: projectId,
e2b_session_id: sandbox.sandboxId,
template: project.template,
status: 'ready',
url: null,
metadata: { restored_from_snapshot: latestSnapshot.snapshot_id },
expires_at: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
};
const session = await createSandbox(sandboxData);
return { sandbox, session, restoredFromSnapshot: true };
} catch (error) {
console.error('[E2B] Failed to restore from snapshot, falling back to template:', error);
// Fall through to template creation
}
}
// No snapshot or restore failed - create from template
return this._createSandbox(projectId, project, teamApiKey);
}
/**
* Save sandbox state before shutdown
*/
static async saveAndStopSandbox(sandbox: Sandbox, projectId: string, sessionId: string) {
try {
// Auto-save snapshot
const snapshotId = await SnapshotService.autoSave(sandbox, projectId);
if (snapshotId) {
console.log('[E2B] Saved snapshot before shutdown:', snapshotId);
}
// Kill sandbox
await sandbox.kill();
await updateSandbox(sessionId, {
status: 'removed',
stopped_at: new Date().toISOString(),
});
} catch (error) {
console.error('[E2B] Error during save and stop:', error);
throw error;
}
}
}Create: src/server/actions/snapshots.ts
'use server';
import { authActionClient } from '@/lib/clients/action';
import { SnapshotService } from '@/lib/services/snapshot-service';
import { E2BService } from '@/lib/services/e2b-service';
import { getProject } from '@/lib/db/projects';
import { z } from 'zod';
const createSnapshotSchema = z.object({
projectId: z.string().uuid(),
description: z.string().optional(),
});
export const createProjectSnapshot = authActionClient
.schema(createSnapshotSchema)
.metadata({ actionName: 'createProjectSnapshot' })
.action(async ({ parsedInput, ctx }) => {
const { projectId, description } = parsedInput;
const { supabase } = ctx;
// Get active sandbox
const { sandbox } = await E2BService.getOrCreateSandbox(projectId, supabase);
// Create snapshot
const snapshotId = await SnapshotService.createSnapshot(
sandbox,
projectId,
description
);
return { snapshotId };
});
const restoreSnapshotSchema = z.object({
snapshotId: z.string(),
projectId: z.string().uuid(),
});
export const restoreFromSnapshot = authActionClient
.schema(restoreSnapshotSchema)
.metadata({ actionName: 'restoreFromSnapshot' })
.action(async ({ parsedInput, ctx }) => {
const { snapshotId, projectId } = parsedInput;
const { supabase } = ctx;
const project = await getProject(projectId);
if (!project) {
throw new Error('Project not found');
}
const teamApiKey = await TeamApiKeyService.getTeamApiKey(project.team_id, supabase);
// Restore sandbox from snapshot
const sandbox = await SnapshotService.restoreFromSnapshot(snapshotId, teamApiKey);
return { sandboxId: sandbox.sandboxId };
});
export const listProjectSnapshots = authActionClient
.schema(z.object({ projectId: z.string().uuid() }))
.metadata({ actionName: 'listProjectSnapshots' })
.action(async ({ parsedInput }) => {
const { projectId } = parsedInput;
const snapshots = await SnapshotService.getProjectSnapshots(projectId);
return { snapshots };
});Add snapshot controls to workspace UI:
// In workspace header or sidebar
<Button onClick={() => createProjectSnapshot({ projectId })}>
Save Snapshot
</Button>
<DropdownMenu>
<DropdownMenuTrigger>Load Snapshot</DropdownMenuTrigger>
<DropdownMenuContent>
{snapshots.map((snapshot) => (
<DropdownMenuItem
key={snapshot.id}
onClick={() => restoreFromSnapshot({ snapshotId: snapshot.snapshot_id, projectId })}
>
{snapshot.description} - {new Date(snapshot.created_at).toLocaleString()}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>// In workspace cleanup/beforeunload handler
await E2BService.saveAndStopSandbox(sandbox, projectId, sessionId);
// Snapshot is auto-saved, user can resume later// When opening workspace
const { sandbox, restoredFromSnapshot } = await E2BService.getOrCreateSandboxWithSnapshot(projectId);
if (restoredFromSnapshot) {
toast.success('Restored from your last session!');
}// User clicks "Save Snapshot" button
await createProjectSnapshot({
projectId,
description: 'Before major refactor',
});Runtime snapshots require E2B SDK v0.17.0+. Check current version:
npm list e2bUpdate if needed:
npm install e2b@latest- Snapshots persist in E2B infrastructure until explicitly deleted
- You should implement cleanup for old snapshots (e.g., keep last 10)
- Snapshots count against storage quota
- Template snapshots: Free, included in template
- Runtime snapshots: May incur storage costs depending on E2B pricing
- Check E2B dashboard for snapshot storage usage
Template Snapshots (Already Working):
- Created once during template build
- Used every time a new sandbox starts
- Fast boot times for all users
Runtime Snapshots (This Implementation):
- Created during user's work session
- Saves user's specific changes and state
- Allows resume exactly where left off
- Requires database table + new API calls
Both work together:
- User creates project → Boots from template snapshot (fast!)
- User makes changes → Saves runtime snapshot when done
- User returns → Restores from runtime snapshot (exact state!)