FullStacked Runtime
FullStacked Cloud projects run on the FullStacked runtime (fullstacked npm package, version 1.0.0-alpha.1844 on this platform). A project is one bundled ES module: UI code (DOM, React) and "backend" code (Node built-ins, database drivers, process.env) run in the same JavaScript context, bridged to a native Go core. There is no separate server process: never start an HTTP server, call your own functions directly. This page lists what the runtime actually provides so code written for it works the first time.
Execution model
| Fact | Consequence for your code |
|---|---|
One entrypoint per project: the first of index.tsx, index.ts, index.jsx, index.js found at the project root | Import everything else from it. When both index.tsx and index.ts exist, index.tsx is the entrypoint. |
The entrypoint is always compiled with the classic JSX transform (React.createElement), whatever tsconfig.json says; other files follow tsconfig.json jsx (classic when unset) | Start every file that contains JSX with import React from "react";. Without it the app fails with ReferenceError: React is not defined. |
esbuild bundles the entrypoint (ES module, TypeScript and JSX built in) into out/, with a generated out/index.html that only loads the bundle's script and stylesheet: it has no root element | There is no HTML file to write. Create your root element in code (see below). CSS you import is bundled and linked. |
Only .png, .jpg, .jpeg, .gif and .svg imports have a loader (they resolve to file URLs) | Other asset types (.webp, fonts, media), including those referenced by CSS url(), fail the build with "No loader is configured". out/ is deleted and rebuilt on every bundle: keep static files at the project root, where fetch("/<file>") finds them. |
| The bundle targets the browser platform | npm packages resolve to their browser builds (for example ws resolves to its browser stub, which throws: use the global WebSocket). |
| UI and logic share one context | Put data access in plain async functions (for example db.ts or actions/<name>.ts) and call them from components. No REST layer, no express, no http.createServer. |
process.env comes from the runtime (Client GUI launch, .env + -e flags, or the action runner) | Read process.env.* anywhere. Never hardcode platform hosts. See Injected process.env. |
| The filesystem is rooted at the project directory | fs paths starting with / resolve inside the project, relative paths against process.cwd() (which starts at /, the project root). A path that would leave the project resolves to the project root instead of failing: fs.rm("..") deletes the whole project. |
// index.tsx: the single entrypoint
import React from "react";
import { createRoot } from "react-dom/client";
import { App } from "./app";
const root = document.createElement("div");
document.body.append(root);
createRoot(root).render(<App />);Running a project
npx fullstacked # bundle and run the project in the current directory (port 9000 by default)
npx fullstacked --port 3000 # another starting port (-p is --plugin, not the port)
npx fullstacked -b # bundle only
npx fullstacked -f script.ts # bundle and run one script file, then exit
npx fullstacked -e AUTH_TOKEN=$AUTH_TOKEN -e KEY=value<project>/.env is loaded first, then every -e KEY=VALUE (which wins); with -f, .env is read from the current directory. In the FullStacked Cloud Client GUI the environment is injected for you.
Module resolution
The bundler maps runtime modules before npm resolution: it strips a node: prefix and a fullstacked/ prefix, then looks the name up in the runtime's module table. Consequences:
node:fs,fsandfullstacked/fsare the same module;fullstacked/tunnelandtunneltoo.- npm packages whose name collides with a runtime module (
auth,bundle,config,fetch,git,packages,parentWindow,plugin,run,test,tunnel,websocket, or a Node built-in listed on this page) cannot be imported: the runtime module is used instead. - A named import of something a runtime module does not export (
import { createServer } from "http",import { exec } from "child_process",import { copyFile } from "fs") fails the bundle with "No matching export". require()of these modules works in CommonJS dependencies.
Global fetch and WebSocket
At startup the runtime replaces globalThis.fetch and globalThis.WebSocket with implementations executed by the Go core.
| Call | Behavior |
|---|---|
fetch("https://...") / fetch("http://...") | Sent by the Go core: no browser CORS, no preflight, no mixed-content blocking. Custom headers (including Authorization) and methods work. |
fetch("http://<tunnel name>/...") | Routed through the registered tunnel (see fullstacked/tunnel). |
fetch("/path") or fetch("fs://...") | In an app, served by the runtime's local file handler: the file at out/<path>, else <project>/<path>, else not found. It never reaches FullStacked Cloud (in -f scripts and actions it fails). |
fetch("./path") | Throws (invalid URL). Use an absolute URL. |
new WebSocket("wss://..." | "ws://...", protocols?) | Dialed by the Go core, tunnel names included. Standard browser signature only: no custom headers option. |
Pitfalls of the fetch implementation:
Content-Typeis never inferred from the body: set it yourself ("Content-Type": "application/json"withJSON.stringify(...)). AvoidFormDatabodies: no multipart boundary header is generated.response.clone()andresponse.formData()returnnull.json(),text(),arrayBuffer(),blob(),bytes()and streamingresponse.bodywork.- A response header sent several times keeps only its last value (several
Set-Cookieheaders collapse to one). redirect: "manual"or"error"disables following redirects; the default follows them.signal(AbortController) is honored.
const res = await fetch(`${process.env.AUTH_URL}/api/v1/me`, {
headers: { Authorization: `Bearer ${process.env.AUTH_TOKEN}` },
});Node built-in modules
Backed by the Go core
| Module | What exists |
|---|---|
fs, fs/promises | Sync, callback and promise forms of readFile, writeFile, appendFile, readdir (withFileTypes), mkdir (always recursive), rm / unlink (rm is recursive), rename, stat, cp; plus existsSync, statSync, realpathSync, createReadStream, createWriteStream, constants. Not available: copyFile, access, lstat, chmod, chown, utimes, symlink, watch. Copy a file or a directory with cp / cpSync (parent directories are created). |
path | join, resolve, normalize, parse, extname, dirname, basename, isAbsolute, relative, sep; path.posix and path.win32 are the same object. No format or delimiter. |
os | platform(), arch(), endianness(), release(), type(), hostname(), tmpdir() (/.tmp, inside the project). Nothing else (no cpus, homedir, networkInterfaces, EOL). |
net | TCP client only: Socket, connect, createConnection, isIP, isIPv4, isIPv6. No createServer. Database drivers (pg, ioredis, mongodb) use it, including through tunnel names. |
dns, dns/promises | lookup (callback and promise); promises resolve4, resolve6, resolveCname, resolveMx, resolveNs, resolveSrv, resolveTxt. The generic resolve() is a stub: use the typed functions. |
dgram | createSocket("udp4" | "udp6") and Socket. |
crypto | Node's API implemented by the Go core: createHash, createHmac, hash, randomBytes, randomFill(Sync), randomInt, randomUUID, getRandomValues, pbkdf2(Sync), scrypt(Sync), hkdf(Sync), createCipheriv / createDecipheriv, createSign / createVerify, sign / verify, generateKeyPair(Sync), generateKey(Sync), key objects, public/private encrypt and decrypt, Diffie-Hellman and ECDH, timingSafeEqual. X509Certificate and Certificate throw. Do not rely on crypto.subtle / webcrypto: the webview may not be a secure context. |
zlib | Deflate, inflate, gzip, gunzip, unzip, raw variants and Brotli: streams (createGzip, ...), callback and *Sync forms, crc32, constants. |
worker_threads | new Worker(path) (a module worker: point it at a separately bundled .js file, for example from fullstacked/bundle bundleFile) and parentPort inside the worker. message listeners receive a MessageEvent: read event.data. No workerData, isMainThread or MessageChannel. |
process (also the global), in apps | env (read from the runtime on each access: treat it as read-only, assignments are not kept), argv, versions.fullstacked (object with major, minor, patch, build, branch, hash), cwd(), chdir(), nextTick, hrtime (no hrtime.bigint), exit(), stdout / stderr (write goes to the console). process.on(...) and the other event methods are no-ops; version is ""; browser is true; platform / arch are not set (use os). In -f scripts and remote action runs the code runs under Node and process is Node's own. |
fs details:
- Errors carry no
code(noENOENT): the message reads likeerror from bridge: [open /abs/path: no such file or directory]. Test withexistsSyncinstead of catchingENOENT. rmandunlinkof a missing path succeed silently.- The callback forms of
mkdir,rm,unlink,writeFile,appendFile,renameandcptake no options argument:fs.mkdir(p, { recursive: true }, cb)never callscb. Preferfs.promisesor the sync forms.
Pure JavaScript
buffer (global Buffer), events, stream (promises, web, consumers), util (util/types), url, querystring, string_decoder, timers (timers/promises, global setImmediate), assert, readline (readline/promises), perf_hooks, diagnostics_channel, constants, tty, console.
Unavailable
| Module | Behavior | Use instead |
|---|---|---|
http, https, http2 | Export only Agent (no createServer, request, get, STATUS_CODES): http.createServer(...) fails with TypeError: ... is not a function, and a named import of them fails the bundle | fetch; for UI to logic calls, import and call your functions. Libraries that need http.request or http.Agent do not work: use fetch-based clients. |
tls | connect, createServer, createSecureContext throw Error("unavailable") | Keep ssl: false on drivers connected through a tunnel (the tunnel is already wss://). |
child_process, async_hooks, module, vm, v8, sqlite | Any property access on the default import throws the string "unavailable"; a named import fails the bundle | fullstacked/git, fullstacked/packages, fullstacked/bundle, fullstacked/run. |
Any other built-in (cluster, inspector, repl, ...) | Not provided |
Runtime modules (fullstacked/*)
fullstacked/tunnel
import tunnel from "fullstacked/tunnel";
interface Tunnel {
host: string; // hub hostname only: no scheme, no port (use process.env.TUNNELS_HOST)
authorization?: string; // sent verbatim as the Authorization header of the WebSocket upgrade
port?: number; // hub port, default 443 (80 when unsecure)
unsecure?: boolean; // ws:// instead of wss://
path?: string; // hub WebSocket path (FullStacked Cloud: leave unset)
name?: string; // the name to register, default 8 random letters
}
tunnel.register(t: Tunnel): Promise<string>; // resolves with the tunnel nameThe returned name is a virtual host recognized by net (and every TCP driver), fetch and WebSocket; for fullstacked/git, pass it as the tunnel argument (clone(url, dir, { tunnel }), pull(dir, tunnel), ...). The port you give a driver or put in a URL is ignored: the hub picks the target from the token.
How it works:
registerstores the settings under the name and opens the tunnel's first WebSocket session towss://<host>:<port><path>in the background. It never fails on bad credentials: authorization errors surface on the first connection.- Each registered tunnel keeps its own WebSocket sessions to the hub, authenticated once at their upgrade. Every connection to the name (socket,
fetch,WebSocket) is a stream multiplexed on one of them: no handshake per connection. The runtime adds sessions under load (up to 8, 32 streams each) and closes idle ones. - Registering the same
nameagain with identical settings reuses its sessions; different settings replace them (the old sessions close once their streams end). The registry is shared by every project context of the runtime. Register once and reuse the name.
With FullStacked Cloud, authorization is `${process.env.AUTH_TOKEN}:${process.env.<X>_TOKEN}`: see Managed Data and Edge & LAN Tunnels.
Other modules
| Import | API |
|---|---|
fullstacked/auth | authenticate(url: string, opts?: { windowFeatures?: string }): Promise<any>: opens url with auth=1 added in the platform's auth surface and resolves with what the page posts back. Needs a window (apps only). See Runtime Sign-In. |
fullstacked/config | get(key), set(key, value: string), list(), delete(key) (alias del): string key/value store of the runtime context, persisted in the project's .git/config.json. |
fullstacked/git | The directory comes first (clone(url, dir, { tunnel?, proxy? }) excepted): hasGit(dir), init(dir, url), head(dir), status(dir), add(dir, path), log(dir) (the last 10 commits), commit(dir, message, { name, email }), pull(dir, tunnel?), fetch(dir, tunnel?), push(dir, tunnel?), reset(dir, hard, ...files), branch(dir), tags(dir), checkout(dir, ref, create?, tunnel?), merge(dir, branch), restore(dir, ...files), setConfig(dir, key, value). Network calls return progress streams. Credentials come from a git-auth plugin. |
fullstacked/packages | install(dir, saveDev: boolean, ...names) (no names: the dependencies of package.json), uninstall(dir, ...names), audit(dir), resolve(moduleName, startDir?), addNodePath(path). Install and uninstall return a progress event emitter. |
fullstacked/bundle | bundle(entryPoint?) (a directory or file, default .), bundleFile(entryPoint), esbuildVersion(). Resolve with { OutputFiles, Errors, Warnings }. |
fullstacked/run | run(directory | { directory?, env?, safe? }): Promise<number>: creates a runtime context for the project in directory and resolves with its id. It does not open it: pass the id to globalThis.fullstacked.open(id) to display the project. |
fullstacked/plugin | register("git-auth", { callback: (host) => ({ username, password }) }), register("build", { data: { filter, ext? }, callback }), register("deeplink", { callback: (url) => void }); resolves with { unregister() }. deeplink(url, ctx?) triggers deeplink plugins. |
fullstacked/parentWindow | disableAutoWindowSize(): stop saving the window size when it is resized (the saved size is restored at startup, before your code runs). |
fullstacked | execute(command, opts?) runs a fullstacked ... CLI command line in-process; validateCommand(command) returns its tokens from fullstacked on, or null. |
Checklist for agents
- One entrypoint (
index.tsxfor apps with UI); create the root element in code. import React from "react";in every file that contains JSX, the entrypoint included.- Call your own functions, never a local HTTP server; call FullStacked Cloud with absolute URLs built from
process.env. - Set
Content-Typeon everyfetchwith a body. - Use
fs.promisesor the sync forms listed above; copy withcp, notcopyFile; check paths withexistsSync. - Reach managed data and LAN services only through
fullstacked/tunnelwithssl: falseon the driver. - No
child_process,http.request,tls, or npm packages that depend on them.