Puter is an open-source internet operating system — a complete desktop environment that runs in your web browser. It gives every user a personal cloud with files, apps, and settings that follow them to any device, and it gives developers a single JavaScript library (puter.js) that adds cloud storage, a key-value database, authentication, web hosting, serverless functions, and frontier AI models to any website with no backend, no API keys, and no server bills.
This guide is for the people who use Puter: end users of the desktop at puter.com, developers who build apps on top of puter.js, operators who self-host the platform, and anyone driving Puter from the puter command-line tool. It covers everything you can do with Puter, exactly as the product behaves at the documented commit — every SDK call, every CLI command, every install path, with copy-paste-runnable examples.
Note
Puter has four faces: the desktop (the GUI at puter.com or your self-hosted instance), the
puter.jsSDK (the library third-party apps use), theputerCLI (deploy sites and workers from a terminal), and the self-hostable backend (run the whole thing yourself). This guide walks through all four. If you only want to add AI or cloud features to a website, jump straight to Building with puter.js.
1. What Puter Is
Puter is several things at once, and which one matters depends on who you are.
| If you are… | Puter is… | Start at |
|---|---|---|
| A person who wants a free personal cloud | A browser-based desktop with files, apps, and settings | Using the Desktop |
| A web developer | A zero-backend SDK for storage, auth, hosting, and AI | Building with puter.js |
| Someone who wants AI in their app without API keys | A gateway to GPT, Claude, Gemini, DALL·E, and more — paid by the user, not you | The User-Pays Model |
| A self-hoster or homelab operator | A full open-source stack you run with one Docker command | Self-Hosting Puter |
| A terminal user | A CLI to publish static sites and deploy serverless workers | The puter CLI |
The core idea
The web has always had a backend problem: to ship anything beyond a static page you need a server, a database, authentication, file storage, and — increasingly — paid AI APIs. Puter collapses all of that into one script tag. You write front-end code that calls puter.fs.write(...), puter.kv.set(...), puter.ai.chat(...), or puter.hosting.create(...), and Puter's cloud handles the storage, identity, and billing.
The model that makes this work is user-pays: when your app calls a paid resource (AI, storage, hosting), the cost is billed to the end user's Puter account, not to you. You ship apps with AI and cloud features and pay nothing for usage. See The User-Pays Model.
What's in the box
- Desktop GUI — a windowing desktop environment with a file manager, apps, settings, themes, and a taskbar, served as a single web app.
puter.js— the official client SDK (npm package@heyputer/puter.js, or<script src="https://js.puter.com/v2/">). Modules:ai,fs,kv,hosting,auth,apps,ui,workers,net,peer,perms, plus utilities.puterCLI — the@heyputer/clinpm package; deploy static sites and serverless workers from a terminal.- Serverless Workers — deploy a single JavaScript file that becomes an HTTP endpoint with a built-in router and access to your Puter resources.
- Backend — a Node.js + Express server you can self-host, with pluggable databases (SQLite, MySQL/MariaDB, PostgreSQL), object storage (S3-compatible), and AI providers.
Tip
Everything in Puter is free and open-source under the AGPL-3.0 license (the
puterCLI is MIT). You can use the hosted service at puter.com or run your own instance — the SDK and desktop behave the same either way.
2. Quick Start
There are three independent ways to start with Puter. Pick the one that matches your goal.
Use the hosted desktop
Open puter.com in any modern browser, create a free account, and you have a personal cloud desktop. Nothing to install.
Add AI/cloud to a website
Drop one script tag into any HTML page and start calling the SDK — no signup, no API key:
<!DOCTYPE html>
<html>
<head>
<script src="https://js.puter.com/v2/"></script>
</head>
<body>
<script>
puter.ai.chat("Why is the sky blue?").then((response) => {
puter.print(response);
});
</script>
</body>
</html>
Open the file through a local web server (not file:// — see Troubleshooting) and the answer prints to the page. The first paid call prompts the visitor to sign in to Puter so the cost is billed to them.
Self-host the whole platform
With Docker installed, one command brings up the full stack:
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
On Windows (PowerShell):
irm https://raw.githubusercontent.com/HeyPuter/puter/main/install.ps1 | iex
Then open http://puter.localhost once the container reports healthy. See Self-Hosting Puter for details.
Your first puter.js app in three lines
Every puter.js call returns a promise. The pattern is always the same — load the SDK, call a module method, handle the result:
<script src="https://js.puter.com/v2/"></script>
<script>
// Cloud storage: write a file to the user's Puter drive
await puter.fs.write('hello.txt', 'Hello, Puter!');
// Key-value store: save a setting
await puter.kv.set('theme', 'dark');
// AI: ask a question
const reply = await puter.ai.chat('Give me a fun fact.');
puter.print(reply);
</script>
A fun fact prints directly into the page body.
Important
puter.jsmust be served overhttp://orhttps://. Opening an HTML file directly from disk (file://) is unsupported — Puter shows an "Unsupported Protocol" warning. Use any static server:npx http-server, VS Code Live Server, orpython -m http.server.
3. Installation & Loading the SDK
Loading puter.js in the browser
The simplest and recommended method is the CDN script tag, which always serves the current v2 build:
<script src="https://js.puter.com/v2/"></script>
After it loads, a global puter object is available everywhere — window.puter — with every module attached (puter.ai, puter.fs, and so on).
Installing from npm
For bundled apps (webpack, Vite, etc.) or Node.js usage, install the package:
npm install @heyputer/puter.js
import puter from '@heyputer/puter.js';
const reply = await puter.ai.chat('Hello from a bundled app!');
console.log(reply);
Note
The package exposes full TypeScript types (
index.d.ts). Importing it in a TypeScript project gives you typed signatures for every module —ChatOptions,WriteOptions,FilePickerOptions, and dozens more.
Where the SDK runs
puter.js detects its environment automatically and adapts. You don't configure this — it's listed here so you understand its behavior:
| Environment | When it applies | Notes |
|---|---|---|
gui |
Loaded inside the Puter desktop itself | Uses the desktop's auth token directly |
app |
Your app running in a window/iframe inside Puter | Receives auth + app identity from the host |
web |
A third-party website (the common case) | Shows a sign-in popup on the first paid call |
nodejs |
Node.js script | Polyfills browser globals; auth via stored token |
web-worker / service-worker |
Inside a Web/Service Worker | Used by Puter Workers |
Tip
Read the current environment any time with
puter.env. It returns one of the strings above.
Installing the CLI
The puter command-line tool is a separate npm package:
npm install -g @heyputer/cli
Adds the `puter` command to your PATH.
Requires Node.js 18 or newer. See The puter CLI.
4. Using the Puter Desktop
The desktop is the GUI you see at puter.com (or at your self-hosted domain). It is a full windowing environment built as a single web application.
Signing in
Visit the site, click Sign Up, and create an account with a username and password. On a self-hosted instance the first-boot admin account is created automatically and its password is printed once to the container log (see Self-Hosting).
Note
A self-hosted instance prints the admin credentials exactly once at first boot:
************************************************************ * Your default login credentials are: * Username: admin * Password: <hex string> ************************************************************Save that password — it is not shown again.
Your home folders
Every account gets a personal file tree. The standard folders are:
| Folder | Path | Purpose |
|---|---|---|
| Home | /{username} or ~ |
Your root directory |
| Desktop | ~/Desktop |
Files and shortcuts shown on the desktop |
| Documents | ~/Documents |
General document storage |
| Public | ~/Public |
Files you intend to share |
| AppData | ~/AppData/{app-id} |
Per-app private storage (apps write here automatically) |
| Sites | ~/Sites |
Static websites you've published |
| Workers | ~/Workers |
Serverless worker source files |
The ~ shorthand expands to your home directory in every SDK path. AppData is sandboxed per app — an app you run only sees its own ~/AppData/{its-id} folder unless you grant broader access.
Working with windows and apps
The desktop supports multiple resizable, draggable windows; a taskbar; right-click context menus; and a settings panel for themes, language, and account management. Apps launched inside Puter run in sandboxed iframes and talk to the desktop through puter.js.
Installing and publishing apps
Apps in Puter are web apps registered as desktop entries. You can browse and install community apps from the Puter App Store, and you can publish your own — any site built with puter.js can be registered as a Puter app. See Building and Publishing Apps for the SDK side.
5. Building with puter.js
This is the heart of Puter for developers. puter.js exposes its capabilities as modules hanging off the global puter object. Every method returns a promise, so you can await it or use .then().
The modules are:
| Module | Object | What it does |
|---|---|---|
| AI | puter.ai |
Chat (GPT/Claude/Gemini/…), image gen, TTS, transcription, OCR, video gen |
| Cloud Storage | puter.fs |
Read/write/move/copy/delete files and directories |
| Key-Value Store | puter.kv |
Per-app NoSQL key-value database |
| Hosting | puter.hosting |
Publish static sites to *.puter.site subdomains |
| Auth | puter.auth |
Sign in, sign out, identify the user, read usage |
| Apps | puter.apps |
Create and manage registered Puter apps |
| UI | puter.ui |
Dialogs, windows, pickers, menus, notifications |
| Workers | puter.workers |
Deploy and manage serverless functions |
| Networking | puter.net |
Raw TCP/TLS sockets and a server-side fetch |
| Peer | puter.peer |
Peer-to-peer (WebRTC) data channels |
| Perms | puter.perms |
Request access to the user's files and resources |
| Utilities | puter |
print(), randName(), env, appID |
5.1 AI — puter.ai
The AI module is the most popular reason to use Puter: it gives any website access to frontier models with no API keys and no per-token bill to you.
Chat — puter.ai.chat()
The flexible entry point for text and vision models. Call shapes:
// Simplest: a single prompt string
const answer = await puter.ai.chat('What is the capital of France?');
puter.print(answer);
// Choose a model
await puter.ai.chat('Explain quantum tunneling', {
model: 'claude-sonnet-4-6',
});
// Full message array (system + user turns)
await puter.ai.chat([
{ role: 'system', content: 'You are a terse assistant.' },
{ role: 'user', content: 'Summarize the French Revolution.' },
]);
// Vision: pass an image URL as the second argument
await puter.ai.chat("What's in this picture?", 'https://example.com/cat.jpg');
Paris (and so on, depending on the prompt).
Streaming — set stream: true to receive the response incrementally as it's generated:
const response = await puter.ai.chat('Write a short poem about the sea.', {
stream: true,
});
for await (const part of response) {
puter.print(part?.text);
}
Function calling — pass a tools array describing functions the model can call, then execute the requested tool and feed the result back. Puter supports both single and streaming function-calling flows.
Tip
Puter routes to many providers behind
chat(): OpenAI (GPT family, o3-mini), Anthropic (Claude Sonnet), Google (Gemini), DeepSeek, xAI (Grok), Mistral, Groq, Together, OpenRouter, and more. Useputer.ai.listModels()to see exactly which model IDs your instance exposes — never hard-code a list.
Discovering models and providers:
const models = await puter.ai.listModels(); // all chat models, grouped by provider
const providers = await puter.ai.listModelProviders(); // provider names only
Image generation — puter.ai.txt2img()
Generate an image from a text prompt. Returns an image element/URL you can display:
const image = await puter.ai.txt2img('A serene mountain lake at sunrise');
document.body.appendChild(image);
The optional second argument carries options (model, size, image-to-image input). For multi-turn image editing, keep the conversation going with the same model.
Text-to-speech — puter.ai.txt2speech()
Convert text to spoken audio. Supports multiple engines and voices:
const audio = await puter.ai.txt2speech('Hello, world!');
audio.play();
// With options: pick a provider/voice
await puter.ai.txt2speech('Bonjour le monde', {
engine: 'elevenlabs', // or 'openai', 'gemini', 'xai', 'aws'
});
Discover what's available:
const engines = await puter.ai.txt2speech.listEngines(); // available TTS engines/models
const voices = await puter.ai.txt2speech.listVoices(); // available voices per provider
Transcription — puter.ai.speech2txt()
Turn an audio recording into text. Returns a Speech2TxtResult:
const result = await puter.ai.speech2txt(audioFileOrBlob);
puter.print(result.text);
Voice changer — puter.ai.speech2speech()
Transform an audio clip into a different voice (provided by ElevenLabs-class engines), accepting either a URL or a stored file.
OCR — puter.ai.img2txt()
Extract text from an image:
const text = await puter.ai.img2txt('https://example.com/receipt.png');
puter.print(text);
Video generation — puter.ai.txt2vid()
Generate video from a text prompt, with an options form for model and parameters:
const video = await puter.ai.txt2vid('A timelapse of clouds over a city');
document.body.appendChild(video);
Caution
AI calls are paid resources. In the common
webenvironment, the first AI call prompts the visitor to sign in to Puter; the cost is then billed to their account. Your app pays nothing. See The User-Pays Model.
AI quick reference
| Method | Purpose |
|---|---|
puter.ai.chat(prompt, options?) |
Text/vision chat completion (streaming optional) |
puter.ai.chat(messages, options?) |
Multi-turn chat with a message array |
puter.ai.listModels() |
List available chat models by provider |
puter.ai.listModelProviders() |
List available providers |
puter.ai.txt2img(prompt, options?) |
Generate an image from text |
puter.ai.txt2speech(text, options?) |
Generate speech audio from text |
puter.ai.txt2speech.listEngines() |
List TTS engines |
puter.ai.txt2speech.listVoices() |
List TTS voices |
puter.ai.txt2vid(prompt, options?) |
Generate video from text |
puter.ai.img2txt(image) |
Extract text from an image (OCR) |
puter.ai.speech2txt(audio) |
Transcribe audio to text |
puter.ai.speech2speech(options) |
Change the voice of an audio clip |
5.2 Cloud Storage — puter.fs
A full cloud filesystem scoped to the signed-in user. Paths use ~ for the home directory and forward slashes.
// Write a file (creates or overwrites)
await puter.fs.write('~/Documents/notes.txt', 'My notes');
// Read a file (returns a Blob)
const blob = await puter.fs.read('~/Documents/notes.txt');
const text = await blob.text();
puter.print(text);
// Make a directory
await puter.fs.mkdir('~/Projects');
// List a directory
const items = await puter.fs.readdir('~/Documents');
for (const item of items) puter.print(item.name);
// Get metadata about a file or folder
const info = await puter.fs.stat('~/Documents/notes.txt');
// Rename, copy, move, delete
await puter.fs.rename('~/Documents/notes.txt', 'old-notes.txt');
await puter.fs.copy('~/Documents/old-notes.txt', '~/Projects/');
await puter.fs.move('~/Projects/old-notes.txt', '~/Documents/');
await puter.fs.delete('~/Documents/old-notes.txt');
Useful options:
createMissingParents: true— create intermediate folders (mkdir -psemantics) forwrite,mkdir,move,upload.dedupeName: true— auto-number the new item (file,file (1), …) instead of overwriting.overwrite: true— replace an existing file.
// Write into a directory that doesn't exist yet
await puter.fs.write('~/a/b/c/deep.txt', 'content', { createMissingParents: true });
// Create a uniquely-named folder every time
const folder = await puter.fs.mkdir('~/Uploads/batch', { dedupeName: true });
Uploading browser files — puter.fs.upload() accepts File/Blob objects, e.g. from an <input type="file">:
const input = document.querySelector('input[type=file]');
await puter.fs.upload(input.files, '~/Uploads/');
Sharing / public URLs — puter.fs.getReadURL() returns a signed URL you can hand out to read a file without authentication:
const url = await puter.fs.getReadURL('~/Public/photo.png');
Warning
Known issue: at the documented commit,
puter.fs.mkdir(..., { dedupeName: true })does not always auto-number when the target already exists (#3201). If you rely on unique folder names, verify the returned path and retry with a different name if needed.
FS quick reference
| Method | Purpose |
|---|---|
write(path, data, options?) |
Create or overwrite a file |
read(path) |
Read a file as a Blob |
mkdir(path, options?) |
Create a directory |
readdir(path) |
List directory contents (array of FSItem) |
rename(path, newName) |
Rename in place |
copy(source, dest, options?) |
Copy a file/dir |
move(source, dest, options?) |
Move a file/dir |
stat(path) |
Get file/dir metadata (FSItem) |
delete(path, options?) |
Delete a file/dir |
upload(files, dest, options?) |
Upload File/Blob objects |
getReadURL(path) |
Get a signed, shareable read URL |
5.3 Key-Value Store — puter.kv
A per-app NoSQL key-value database — ideal for app settings, user data, and small documents. Values can be strings, numbers, or JSON-serializable objects.
await puter.kv.set('username', 'alice');
const name = await puter.kv.get('username'); // 'alice'
await puter.kv.incr('visits'); // atomic +1
await puter.kv.decr('credits', 5); // atomic -5
await puter.kv.del('username'); // delete one key
const keys = await puter.kv.list(); // list keys
await puter.kv.flush(); // delete everything (this app)
Expiration (TTL):
await puter.kv.set('otp', '123456');
await puter.kv.expire('otp', 300); // expires in 300 seconds
await puter.kv.expireAt('otp', 1893456000); // expires at a UNIX timestamp
Nested updates — incr/decr can target a path inside an object value, and update/remove edit nested fields:
await puter.kv.set('player', { score: 0, lives: 3 });
await puter.kv.incr('player.score', 10);
await puter.kv.update('player.lives', 2);
await puter.kv.remove('player.score');
Listing with patterns and pagination — design your keys for prefix queries (e.g. user:alice:post:001). list() returns keys in lexicographic order and supports a cursor + limit for pagination. Zero-pad numeric keys so they sort correctly.
Tip
The store enforces limits exposed as constants:
puter.kv.MAX_KEY_SIZEandputer.kv.MAX_VALUE_SIZE. Read them rather than guessing.
KV quick reference
| Method | Purpose |
|---|---|
set(key, value) |
Store a value |
get(key) |
Retrieve a value |
incr(key, amount?) / decr(key, amount?) |
Atomic increment/decrement (supports nested paths) |
add(key, value) |
Add a value to a key |
update(path, value) |
Update a nested path |
remove(path) |
Remove a nested path |
del(key) |
Delete a key |
list(options?) |
List keys (supports pagination) |
flush() |
Clear all keys for this app |
expire(key, seconds) / expireAt(key, ts) |
Set TTL / absolute expiry |
MAX_KEY_SIZE / MAX_VALUE_SIZE |
Size limits (constants) |
5.4 Apps — puter.apps
Register and manage Puter apps (desktop-OS entries that point at a hosted URL):
// Register an app pointing at a URL
const app = await puter.apps.create('my-app', 'https://example.com');
// List your apps
const apps = await puter.apps.list();
// Get, update, delete
await puter.apps.get('my-app');
await puter.apps.update('my-app', { title: 'My Great App' });
await puter.apps.delete('my-app');
This is how apps end up on the desktop and in the App Store. Typically you publish a static site with puter.hosting, then register it as an app with puter.apps.create().
5.5 Hosting — puter.hosting
Publish a directory of files to a public *.puter.site subdomain — instant static web hosting:
// Create a directory of files, then host it
await puter.fs.mkdir('~/Sites/my-site', { createMissingParents: true });
await puter.fs.write('~/Sites/my-site/index.html', '<h1>Hello world!</h1>');
// Publish it
const site = await puter.hosting.create('my-unique-subdomain', '~/Sites/my-site');
// → live at https://my-unique-subdomain.puter.site
// Manage sites
const sites = await puter.hosting.list();
await puter.hosting.get('my-unique-subdomain');
await puter.hosting.update('my-unique-subdomain', '~/Sites/new-folder');
await puter.hosting.delete('my-unique-subdomain');
Note
The CLI wraps this whole flow into a single
puter site deploycommand — see The puter CLI.
5.6 Auth — puter.auth
Identify and manage the signed-in user:
// Trigger the sign-in popup (web environment)
await puter.auth.signIn();
// Check status and identity
const signedIn = puter.auth.isSignedIn();
const user = await puter.auth.getUser(); // { username, ... }
// Sign out
await puter.auth.signOut();
// Usage reporting
const usage = await puter.auth.getMonthlyUsage();
const detail = await puter.auth.getDetailedAppUsage();
In the web environment you rarely need to call signIn() explicitly — the first paid call (AI, FS, KV) automatically surfaces the consent popup. Use getUser() to greet the user or branch on identity.
5.7 UI — puter.ui
Dialogs, windows, pickers, and desktop integration. The most-used calls:
// Dialogs
await puter.ui.alert('Saved!');
const name = await puter.ui.prompt('What is your name?');
puter.ui.notify('Upload complete'); // desktop notification
// Pickers
const file = await puter.ui.showOpenFilePicker();
const target = await puter.ui.showSaveFilePicker();
const dir = await puter.ui.showDirectoryPicker();
const color = await puter.ui.showColorPicker();
const font = await puter.ui.showFontPicker();
// Spinners
puter.ui.showSpinner();
puter.ui.hideSpinner();
// Context menus and menubars
puter.ui.contextMenu({ items: [/* ... */] });
puter.ui.setMenubar({ items: [/* ... */] });
When your app runs inside the Puter desktop, the UI module also controls your window — setWindowTitle(), setWindowSize(), setWindowPosition(), showWindow()/hideWindow(), plus lifecycle events:
puter.ui.onWindowClose(() => { /* save before close */ });
puter.ui.onLaunchedWithItems((items) => { /* opened with files */ });
puter.ui.launchApp('editor') opens another Puter app, and puter.ui.socialShare(...) triggers a share sheet.
Note
Known behavior at the documented commit:
showOpenFilePicker/showSaveFilePickermay not behave as expected in the standalonewebenvironment (#2897). They are most reliable for apps running inside the Puter desktop.
UI quick reference (selected)
| Method | Purpose |
|---|---|
alert(message, buttons?) |
Modal alert dialog |
prompt(message) |
Text-input dialog |
notify(message) |
Desktop notification |
contextMenu(options) |
Right-click menu |
setMenubar(options) |
App menubar |
showOpenFilePicker() / showSaveFilePicker() |
File pickers |
showDirectoryPicker() |
Folder picker |
showColorPicker() / showFontPicker() |
Color / font pickers |
showSpinner() / hideSpinner() |
Loading overlay |
createWindow(options) |
New window |
setWindowTitle/Size/Position(...) |
Window control |
launchApp(name, options?) |
Launch another Puter app |
socialShare(...) |
Share sheet |
authenticateWithPuter() |
Drive the sign-in popup directly |
5.8 Serverless Workers — puter.workers
Deploy a single JavaScript file as an HTTP endpoint. See the full Workers programming model in Serverless Workers; the SDK side manages deployments:
// Deploy a worker from a file already in your Puter drive
const deployment = await puter.workers.create('my-api', '~/Workers/my-api.js');
// → reachable at https://my-api.puter.work
const workers = await puter.workers.list();
await puter.workers.get('my-api');
await puter.workers.delete('my-api');
// Call an authenticated worker
const result = await puter.workers.exec('https://my-api.puter.work/endpoint');
5.9 Networking — puter.net
Low-level network primitives unavailable to ordinary browser JavaScript:
// Raw TCP socket
const socket = new puter.net.Socket('example.com', 8080);
// TLS socket
const tls = new puter.net.tls.TLSSocket('example.com', 443);
// Server-side fetch (bypasses browser CORS)
const res = await puter.net.fetch('https://api.example.com/data');
These route through Puter's relay so a browser app can open real TCP/TLS connections and fetch cross-origin resources.
5.10 Peer — puter.peer
Peer-to-peer (WebRTC) data channels for direct browser-to-browser communication:
const server = await puter.peer.serve(); // become a peer others can connect to
const conn = await puter.peer.connect(peerId);
await puter.peer.ensureTurnRelays(); // ensure TURN relays for NAT traversal
5.11 Permissions — puter.perms
When your app needs broader access than its own sandbox, request it explicitly. The user is prompted to grant or deny:
await puter.perms.requestReadDocuments();
await puter.perms.requestWritePictures();
await puter.perms.requestEmail();
await puter.perms.request(/* a specific permission */);
The full set covers Desktop, Documents, Pictures, Videos, Apps, and Subdomains, each in read and write/manage variants, plus requestEmail().
5.12 Utilities
Convenience helpers on the root puter object:
puter.print('Hello'); // append to the page (HTML or escaped/code)
const name = puter.randName(); // e.g. "clever-idea-1234" — unique friendly names
const env = puter.env; // 'gui' | 'app' | 'web' | 'nodejs' | ...
const appID = puter.appID; // this app's unique ID
puter.quiet = true; // suppress the dev console CTA
6. The User-Pays Model
This is the business model that makes Puter unusual, and understanding it is essential to building with the AI and cloud APIs.
You don't pay for your users' usage. When an app built with puter.js calls a paid resource — an AI model, file storage, hosting — the cost is billed to the end user's Puter account, not to the developer. The flow:
- Your site loads
puter.jsand calls, say,puter.ai.chat(...). - If the visitor isn't signed in to Puter, a consent popup appears asking them to sign in (or sign up) and authorize your app.
- Once authorized, the call runs and any cost is metered against their account.
- You ship AI- and cloud-powered features with zero API keys and zero usage bills.
Consequences for your code:
- No API keys. You never embed an OpenAI/Anthropic key. There's nothing to leak.
- Expect a sign-in prompt. The first paid call in the
webenvironment surfaces the popup. Design your UI to handle the brief pause. - Let users see their usage.
puter.auth.getMonthlyUsage()andgetDetailedAppUsage()let you show consumption. - Free tiers exist. Puter accounts include free allowances (the platform models subscription tiers like
user_free,temp_free, andunlimited), so casual use of your app often costs the user nothing.
Important
Because the user pays, your app can offer expensive capabilities (GPT-class chat, image and video generation) that would be cost-prohibitive if you paid per call. This is the core value proposition of building on Puter.
7. The puter CLI
The puter command-line tool (@heyputer/cli) lets you publish static sites and deploy serverless workers from a terminal — handy for CI/CD and scripting. It is in beta (0.x); behavior may change.
Install and authenticate
npm install -g @heyputer/cli
# Interactive login (opens a browser flow)
puter login
# Non-interactive: pipe a token on stdin (never as an argument)
echo "$PUTER_TOKEN" | puter login --with-token
# Confirm who you are
puter whoami
# Log out
puter logout
Caution
Tokens are read only from stdin, never from
argv— so they don't leak into shell history or process listings.echo $TOKEN | puter login --with-tokenis the correct form.
Publishing a static site
# Deploy a folder to a subdomain (interactive prompts fill in blanks)
puter site deploy ./public my-cool-site
# → live at https://my-cool-site.puter.site
# Manage sites
puter site list
puter site get my-cool-site
puter site delete my-cool-site
puter site deploy uploads the whole directory tree (skipping OS junk like .DS_Store/Thumbs.db), creates a versioned deployment folder so previous versions are preserved, then points the subdomain at the new folder. In interactive mode it suggests a random subdomain and re-prompts on invalid input. In non-interactive mode both the directory and subdomain are required.
Tip
Subdomains accept lowercase letters, numbers, and hyphens (not at the ends). You may paste a full host like
my-site.puter.site— the CLI strips the suffix for you.
Deploying a serverless worker
# Deploy a single JS file as a worker
puter worker deploy ./api.js my-api
# → reachable at https://my-api.puter.work
# Manage workers
puter worker list
puter worker get my-api
puter worker delete my-api
Deploying writes your file to ~/Workers/{name}.js and registers the worker (or updates the file in place if it already exists).
Warning
Beta caveat: there is no readiness signal yet, so the CLI cannot confirm that a worker deploy is live — it reports the URL but you should verify by calling it.
Managing apps
puter app list
puter app get my-app
App commands are read-only in the beta.
CLI command reference
| Command | Purpose |
|---|---|
puter login |
Interactive login |
puter login --with-token |
Login with a token piped on stdin |
puter logout |
Clear the stored login |
puter whoami |
Show the current account |
puter site deploy <dir> <subdomain> |
Publish a static site |
puter site list / get <sub> / delete <sub> |
Manage sites |
puter worker deploy <file> <name> |
Deploy a serverless worker |
puter worker list / get <name> / delete <name> |
Manage workers |
puter app list / get <name> |
List/inspect apps (read-only) |
Tip
Set
PUTER_DEBUG=1to see verbose output (resolved paths, uploaded files, raw API responses) duringsite/workeroperations.
8. Serverless Workers
A Puter Worker is a single JavaScript file you deploy that becomes a live HTTP endpoint at https://{name}.puter.work. It runs in a Worker runtime with a built-in router and direct access to your Puter resources.
The router
Inside a worker, a global router object defines routes. It supports the standard HTTP verbs plus a custom() for anything else:
// my-api.js — deploy with: puter worker deploy ./my-api.js my-api
router.get('/health', () => ({ ok: true }));
router.get('/version', () => 'v1');
// :param captures a path segment
router.get('/posts/:id', ({ params }) => {
return { id: params.id };
});
// Multiple params
router.get('/posts/:id/comments/:cid', ({ params }) => {
return `${params.id}/${params.cid}`;
});
// *wildcard captures the rest of the path
router.get('/files/*path', ({ params }) => {
return new Response(params.path);
});
// POST with a request body
router.post('/echo', async ({ request }) => {
const body = await request.text();
return { body };
});
// Non-standard methods
router.custom('PATCH', '/posts/:id', ({ params }) => ({ patched: params.id }));
Each handler receives an event with:
params— captured route parametersrequest— the standardRequestobject (request.text(),request.json(), headers, …)user— the caller's Puter context when the request carries aputer-authheader;user.puteris their full SDK surfaceme/my/myself— the deployer's own Puter resources (the worker owner)
// Read the caller's identity (authenticated requests)
router.post('/whoami', async ({ user }) => {
if (user) {
const profile = await user.puter.auth.getUser();
return { username: profile.username };
}
return { anonymous: true };
});
// Use the deployer's own KV store
router.get('/kv/:key', async ({ params }) => {
return { value: await me.puter.kv.get(params.key) };
});
Return values
A handler may return:
- A plain object → serialized as JSON
- A string,
Blob,ArrayBuffer,Uint8Array,ReadableStream, orURLSearchParams→ sent as-is - A standard
Responseobject → used directly
CORS is handled automatically (preflight OPTIONS and Access-Control-Allow-Origin: *) unless you set router.handleCors = false.
Environment bindings
Workers expose globals like puter_endpoint and puter_auth so the embedded Puter client knows where and how to authenticate:
router.get('/env', () => ({
endpoint: puter_endpoint,
authPrefix: puter_auth.slice(0, 4),
}));
TypeScript support
The @heyputer/worker-types package provides full types for the worker runtime (Handler, WorkerEvent, the router, env bindings). Route params are inferred from the path: router.get('/posts/:id', ...) types params as { id: string }.
import type { Handler, WorkerEvent } from '@heyputer/worker-types';
const getPost: Handler<{ id: string }> = ({ params }) => ({ id: params.id });
router.get('/posts/:id', getPost);
Calling a worker from puter.js
const result = await puter.workers.exec('https://my-api.puter.work/health');
puter.workers.exec() sends the caller's Puter auth so the worker's user context is populated.
9. Self-Hosting Puter
Run the entire Puter platform on your own machine or server. The only requirement is Docker with the Compose plugin.
One-line install
Linux / macOS
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
Windows (PowerShell)
irm https://raw.githubusercontent.com/HeyPuter/puter/main/install.ps1 | iex
The installer:
- Checks that
docker(with the compose plugin) is present (pluscurl/opensslon Linux). - Creates
./puter-selfhosted/(override withPUTER_DIR). - Downloads
docker-compose.ymlandnginx/nginx.conf. - Generates fresh secrets and writes
.env+puter/config/config.json. - Runs
docker compose up -dand prints how to find the first-boot admin password.
First boot takes ~30 seconds while the database initializes. Then open http://puter.localhost (or your chosen domain/port).
Tunable install options
Set these as environment variables before running the installer:
| Variable | Default | Purpose |
|---|---|---|
PUTER_DIR |
puter-selfhosted |
Install directory |
PUTER_DOMAIN |
puter.localhost |
Domain Puter serves on |
PUTER_PORT |
80 |
HTTP port for nginx |
PUTER_URL |
GitHub raw (main) | Where to fetch the compose/nginx files |
PUTER_FORCE |
0 |
Set to 1 to overwrite existing .env/config.json |
# Example: serve on a custom domain and port
PUTER_DOMAIN=cloud.example.com PUTER_PORT=8080 \
curl -fsSL https://raw.githubusercontent.com/HeyPuter/puter/main/install.sh | sh
Note
Re-running the installer in an already-initialized directory is safe: it keeps your existing secrets and just refreshes the compose file and brings the stack up. Pass
PUTER_FORCE=1to regenerate everything.
Finding the admin password
The first-boot admin credentials are printed once to the container log:
Linux / macOS
cd puter-selfhosted
docker compose logs puter | grep password
Windows (PowerShell)
cd puter-selfhosted
docker compose logs puter | Select-String password
The self-hosted stack
The Compose stack brings up several containers: the puter app server, a MariaDB database, Valkey (Redis-compatible cache), a DynamoDB-compatible service, and an S3-compatible object store. The default dev configuration instead uses bundled SQLite and a local fake-S3, so a from-source npm start needs no external services.
Operational details — configuration reference, database engines, scaling, TLS, security hardening, and the full architecture — are in the Admin Guide.
Tip
For a development run from source (not Docker), clone the repo, ensure Node.js 24+, then
npm installandnpm start. The default config (config.default.json) serves on port 4100 atputer.localhostusing SQLite — no external services required.
10. Troubleshooting
Solutions to the most common problems, drawn from the platform's own error messages and community-reported issues.
Error: Unsupported Protocol / puter.js does nothing on a local file
Cause: You opened your HTML file with the file:// protocol. puter.js requires http:// or https://.
Solution: Serve the file from any web server — npx http-server, python -m http.server, or VS Code Live Server — and open it via http://localhost:....
Prevention: Always develop against a local server, even for single-page experiments.
Error: Sign-in popup is blocked or doesn't appear
Cause: The browser blocked the popup, or the call happened before a user gesture.
Solution: Trigger the first paid call (or puter.auth.signIn()) from a click handler so the browser allows the popup. Ensure pop-ups aren't blocked for the site.
Prevention: Gate the first AI/FS/KV call behind an explicit user action.
Error: puter.fs.mkdir with dedupeName doesn't auto-number
Cause: Known issue at the documented commit (#3201) — dedupeName may not always produce a unique name when the folder exists.
Solution: Check the path returned by mkdir(); if it collided, retry with a different base name or add your own suffix.
Prevention: Don't assume dedupeName guarantees uniqueness; verify the returned path.
Error: showOpenFilePicker / showSaveFilePicker not working on the web
Cause: Known issue (#2897) — the pickers are most reliable for apps running inside the Puter desktop, less so in a standalone third-party page.
Solution: For standalone pages, use a native <input type="file"> plus puter.fs.upload() to bring files into Puter.
Prevention: Reserve the UI pickers for apps that run inside the Puter desktop.
Self-host: container reports Up (unhealthy)
Cause: A dependency (database/cache/object store) hasn't finished initializing, or a bind-mounted directory has the wrong permissions (#3191).
Solution: Give it ~30s on first boot. Then check logs: docker compose logs -f puter. On native Linux, ensure the puter/data/* directories are writable (the installer chmod 0777s them to sidestep non-root container UID mismatches).
Prevention: Use the official installer, which pre-creates and permissions the data dirs.
Self-host: can I use Podman instead of Docker?
The installer and compose files target Docker with the Compose plugin. Podman is community-requested (#3170) but not officially documented; you may need to adapt the compose invocation. Use Docker for the supported path.
Error: pip install puter fails / wrong client
Cause: Puter's client is JavaScript (@heyputer/puter.js on npm, or the CDN script tag). There is no official pip package.
Solution: Use the npm package or the <script src="https://js.puter.com/v2/"> tag.
Sign-up problems on a fresh self-host
Cause: Email verification or host/domain configuration (#3127).
Solution: The default OSS config sets strict_email_verification_required: false, so accounts work without email. If you changed domain settings, confirm domain, static_hosting_domain, and the *_app_hosting_domain values match how you reach the instance.
CLI: Cannot log in non-interactively without a token
Cause: You ran puter login in a non-interactive context (CI) without piping a token.
Solution: echo "$PUTER_TOKEN" | puter login --with-token.
11. Frequently Asked Questions
Q: Do I need an API key to use AI with puter.js? No. That's the point — there are no API keys. Paid calls are billed to the end user's Puter account under the user-pays model.
Q: How much does it cost me to add GPT/Claude/Gemini to my site? Nothing for usage. Your users cover the cost through their Puter accounts, which include free allowances.
Q: Which AI models are available?
Many — OpenAI (GPT family, o3-mini), Anthropic (Claude), Google (Gemini), DeepSeek, xAI (Grok), Mistral, and others. Call puter.ai.listModels() for the authoritative, current list rather than hard-coding model IDs.
Q: Is Puter free and open-source? Yes. The platform is AGPL-3.0 (the CLI is MIT). You can use puter.com or self-host.
Q: Can I run Puter entirely on my own server? Yes — one Docker command. See Self-Hosting. It supports SQLite (default dev), MySQL/MariaDB, and PostgreSQL, plus S3-compatible object storage.
Q: Where do files written with puter.fs go?
Into the signed-in user's personal Puter drive, scoped to your app's sandbox (~/AppData/{app-id}) unless you request broader access via puter.perms.
Q: What's the difference between Hosting and Workers?
puter.hosting serves static files at *.puter.site. Workers run your JavaScript as a live HTTP endpoint at *.puter.work with a router and access to Puter resources.
Q: Can I use puter.js in Node.js, not just the browser?
Yes. Install @heyputer/puter.js and import it; the SDK polyfills the browser globals it needs and authenticates via a stored token.
Q: How do I see how much a user has consumed?
puter.auth.getMonthlyUsage() and puter.auth.getDetailedAppUsage().
Q: My app needs to read the user's existing files. How?
Request access explicitly with puter.perms (e.g. requestReadDocuments()); the user is prompted to grant it. By default an app only sees its own AppData sandbox.
Q: Can I publish my puter.js app to the Puter desktop / App Store?
Yes. Host it (with puter.hosting or anywhere), then register it with puter.apps.create(). Submit to the App Store.
Q: Does the SDK support TypeScript?
Fully. @heyputer/puter.js ships type declarations, and @heyputer/worker-types types the Workers runtime.
Q: Where's the official, always-current API reference? docs.puter.com, with a live playground of runnable examples for every API.
12. Reference
SDK module map
| Object | Methods (summary) |
|---|---|
puter.ai |
chat, listModels, listModelProviders, txt2img, txt2speech (+ listEngines/listVoices), txt2vid, img2txt, speech2txt, speech2speech |
puter.fs |
write, read, mkdir, readdir, rename, copy, move, stat, delete, upload, getReadURL |
puter.kv |
set, get, incr, decr, add, update, remove, del, list, flush, expire, expireAt, MAX_KEY_SIZE, MAX_VALUE_SIZE |
puter.hosting |
create, list, delete, update, get |
puter.auth |
signIn, signOut, isSignedIn, getUser, getMonthlyUsage, getDetailedAppUsage |
puter.apps |
create, list, delete, update, get |
puter.ui |
alert, prompt, notify, contextMenu, setMenubar, window control, file/color/font pickers, spinners, launchApp, socialShare, lifecycle events |
puter.workers |
create, delete, list, get, exec |
puter.net |
Socket, tls.TLSSocket, fetch |
puter.peer |
serve, connect, ensureTurnRelays |
puter.perms |
request, requestEmail, request{Read,Write}{Desktop,Documents,Pictures,Videos}, request{Read,Manage}{Apps,Subdomains} |
puter (root) |
print, randName, env, appID, exit |
Domains
| Domain | Used for |
|---|---|
puter.com |
The hosted desktop |
js.puter.com/v2/ |
The SDK CDN |
api.puter.com |
The API origin the SDK calls |
*.puter.site |
Published static sites (puter.hosting) |
*.puter.work |
Deployed workers (puter.workers) |
docs.puter.com |
API docs + playground |
apps.puter.com |
The App Store |
Environments returned by puter.env
gui · app · web · nodejs · web-worker · service-worker
Helpful CLI environment variables
| Variable | Effect |
|---|---|
PUTER_DEBUG=1 |
Verbose CLI output |
PUTER_AUTH_TOKEN |
Provide an auth token to the CLI |
PUTER_SITE_DOMAIN |
Override the site-hosting domain |
PUTER_WORKER_DOMAIN |
Override the worker domain |
Where to get help
- Docs & playground: docs.puter.com
- Discord: discord.com/invite/PQcx7Teh8u
- Reddit: reddit.com/r/puter
- Issues: github.com/HeyPuter/puter/issues
- Security: [email removed]