Projects and storage
The store keeps projects in the browser. The database (IndexedDB) holds the rows: projects, tracks, clips, effects, keyframes, transitions. File storage (the origin private file system, OPFS) holds the media files. Nothing leaves the browser unless your code sends it; see Save and load.
Create the store
createStore opens the namespace's database and returns the store.
import { createStore } from "@vidstudio/engine";
const store = await createStore({ namespace: "my-app" });
const project = await store.projects.create("My project", { frame_rate: 30 });
create takes a name and optional frame_rate, canvas_width and canvas_height. Leave the size out and the project takes it from the first video.
Namespaces
A namespace names one editor's database, file folder and saved preferences. storageNames(namespace) returns the first two.
| Namespace | Database | File folder | Preference keys (local storage) |
|---|---|---|---|
"vidstudio" (the default) |
video-editor |
the root of file storage | ve-new-project-defaults, vidstudio_safe_zone_guide, vidstudio_tour_seen |
"my-app" |
my-app-video-editor |
my-app-files |
my-app:new-project-defaults, my-app:safe-zone-guide, my-app:tour-seen |
The default keeps the names vidstudio.app has always used. Pick your own namespace. Then your data never mixes with another editor's data on the same origin.
Where data lives
| What | Where | Path inside the file folder |
|---|---|---|
| Project rows | the database | |
| Imported media | file storage | projects/<projectId>/media/<assetId>_<extension>.bin |
| Proxies (lighter copies for the preview) | file storage | projects/<projectId>/proxy/<assetId>_proxy.mp4 |
| Exports | file storage | exports/<projectId>/ |
| Viewer preferences | local storage | |
| Downloaded speech models | the browser's cache storage |
A file in file storage is a handle on disk, not bytes in memory. The engine reads only the parts it needs. A one gigabyte recording costs a few megabytes to open.
Quota
The browser sets the quota for the origin. Ask navigator.storage.estimate() for the usage and the quota. When a write is refused, the backend emits a storage-full event and the call rejects.
| Call | Outcome when storage is full |
|---|---|
importVideo, importAudio, importImage, importAsset, importFromUrl |
Rejects with the code import-failed. Event storage-full with operation "import". |
loadDocument |
Rejects. The files written so far are deleted. The project stays as it was. Event storage-full with "import". |
exportProject |
Rejects. Events export-failed and storage-full with "export". The export dialog tells the user the device ran out of storage. |
removeTrack |
Rejects. Event storage-full with "delete_track". |
After its first write, the store sweeps once. It deletes files that no project row points at and that are older than thirty minutes. It also deletes export files older than six hours. Free space in your code with store.deleteProject and backend.discardExportFile.
List and delete projects
store.projects.getAll() returns every project in the namespace, in no fixed order.
import { createStore } from "@vidstudio/engine";
const store = await createStore({ namespace: "my-app" });
const projects = (await store.projects.getAll()).sort((a, b) => b.updated_at - a.updated_at);
for (const project of projects) console.log(project.id, project.name, new Date(project.updated_at));
if (projects.length > 0) {
const removed = await store.deleteProject(projects[0].id);
console.log(`deleted ${removed.assetCount} media files, ${removed.bytesFreed} bytes`);
}
Use store.deleteProject, not store.projects.delete. The first removes the rows, the media folder and the export folder. The second removes only the project row.
Two editors on one page
Give each editor its own store and its own namespace. Pass the namespace to options too, so the saved preferences are separate.
import { useEffect, useState } from "react";
import { VidstudioEditor, createStore } from "@vidstudio/editor";
import "@vidstudio/editor/editor.css";
type Opened = { store: Awaited<ReturnType<typeof createStore>>; projectId: string };
const LEFT = { namespace: "left" };
const RIGHT = { namespace: "right" };
async function open(namespace: string): Promise<Opened> {
const store = await createStore({ namespace });
const project = await store.projects.create("Project", { frame_rate: 30 });
return { store, projectId: project.id };
}
export function TwoEditors() {
const [left, setLeft] = useState<Opened | null>(null);
const [right, setRight] = useState<Opened | null>(null);
useEffect(() => {
open("left").then(setLeft);
open("right").then(setRight);
}, []);
if (!left || !right) return null;
return (
<div style={{ display: "flex", height: "100vh" }}>
<div style={{ flex: 1 }}><VidstudioEditor store={left.store} projectId={left.projectId} options={LEFT} /></div>
<div style={{ flex: 1 }}><VidstudioEditor store={right.store} projectId={right.projectId} options={RIGHT} /></div>
</div>
);
}
Each editor keeps its own media, undo history and keyboard focus. Fonts load once per page and both editors share them.
Your own storage and monitoring
createStore takes services. The backend inherits them from the store.
| Service | Contract | Default |
|---|---|---|
files |
StorageProvider: write, read, delete, createWritable, commit, clearDirectory, list, deleteDirectoryTree, and optional describe |
file storage under the namespace's folder |
monitoring |
MonitoringAdapter: init, captureException, captureMessage, withScope, addBreadcrumb, setTag, setContext, setExtra, setUser |
nothing is reported |
This adapter sends every error the engine catches to the console.
import { createStore, type MonitoringAdapter } from "@vidstudio/engine";
const monitoring: MonitoringAdapter = {
init: () => {},
captureException: (error, hint) => { console.error("[editor]", error, hint?.extra); return undefined; },
captureMessage: (message, level) => { console.log(`[editor] ${level ?? "info"}: ${message}`); return undefined; },
withScope: (callback) => callback({ setTag() {}, setContext() {}, setExtra() {}, setLevel() {}, setUser() {} }),
addBreadcrumb: () => {},
setTag: () => {},
setContext: () => {},
setExtra: () => {},
setUser: () => {},
};
const store = await createStore({ namespace: "my-app", services: { monitoring } });
MemoryFileStorage is a second provider that keeps files in memory, for tests. A worker cannot open a provider without describe. The engine then makes no proxies and runs exports on the page thread.