Agent index (llms.txt) — fetch this first, then the page it names.
Deployments
Deploy applications to the Orama decentralized network. The CLI supports four deployment types: static sites, Next.js SSR, Go backends, and Node.js backends. Each deployment is distributed across nodes, served via Caddy, and assigned a domain automatically.
Deployment Types
| Type | Command | Use Case |
|---|---|---|
| Static | orama deploy static | React, Vue, Svelte, any pre-built SPA or static site |
| Next.js | orama deploy nextjs | Server-rendered Next.js with output: 'standalone' |
| Go | orama deploy go | Go binaries with HTTP server |
| Node.js | orama deploy nodejs | Express, Fastify, any Node.js HTTP server |
Static sites are uploaded to IPFS and served via Caddy. Server-side deployments (Next.js, Go, Node.js) run as managed processes on the node.
Deploy Flags
All deploy commands share these flags:
| Flag | Required | Description |
|---|---|---|
--name | Yes | Deployment name. Used in the domain: <name>-<random>.<base-domain> |
--subdomain | No | Custom subdomain override (default: uses --name) |
--update | No | Update an existing deployment instead of creating a new one |
--env KEY=VALUE | No | Environment variable for a dynamic deployment. Repeatable. First deploy only; refused with --update (see Environment Variables) |
--env-file <path> | No | Read variables from a .env file. First deploy only |
--health-check <path> | No | Path the platform polls to decide the app has started (default /health). Not on static sites |
Next.js has one additional flag:
| Flag | Required | Description |
|---|---|---|
--ssr | No | Deploy with server-side rendering (standalone mode) |
The domain a deployment gets is <name>-<random>.<base-domain>: the examples below leave the short random suffix out for readability, so always use the URL the command prints. See Domains and Routing.
Deploying Static Sites
Deploy static sites built with React, Vue, Angular, or any static site generator.
React/Vite Example
# 1. Build your React app cd my-react-app npm run build # 2. Deploy the build directory orama deploy static ./dist --name my-react-app # Output: # 📦 Creating tarball from ./dist... # ☁️ Uploading to Orama Network... # # ✅ Deployment successful! # # Name: my-react-app # Type: static # Status: active # Version: 1 # Content CID: QmXxxx... # # URLs: # • https://my-react-app.orama.network
What Happens Behind the Scenes
- +Tarball Creation: CLI automatically creates a
.tar.gzfrom your directory - +IPFS Upload: Files are uploaded to IPFS and pinned across the network
- +DNS Record: A DNS record is created pointing
my-react-app.orama.networkto the gateway - +Instant Serving: Your app is immediately accessible via the URL
Features
- +✅ SPA Routing: Unknown routes automatically serve
/index.html(perfect for React Router) - +✅ Correct Content-Types: Automatically detects and serves
.html,.css,.js,.json,.png, etc. - +✅ Caching:
Cache-Control: public, max-age=3600headers for optimal performance - +✅ Zero Downtime Updates: Use
--updateflag to update without downtime
Updating a Deployment
# Make changes to your app # Rebuild npm run build # Update deployment orama deploy static ./dist --name my-react-app --update # Version increments automatically (1 → 2)
Deployment names
A name is 1–56 characters of letters, digits, - and _, starting with a
letter or digit. Anything else is refused with 400 before the upload is
stored: the name becomes part of the app's subdomain (one DNS label) and of the
systemd unit that runs it (orama-deploy-<runtime>@<namespace>-<name>).
A new deployment is refused with 409 when:
- +one with the same name already exists in the namespace — deploy again with
--update; - +its
<namespace>-<name>is already another deployment's unit instance. Namespaceawith nameb-cand namespacea-bwith namecwould share one unit, one directory and one environment file, so the second is refused and has to choose a different name.
The second check is made on the node itself, not only in the namespace's
registry: every gateway on a node shares the deployments directory, but each
reads only its own deployments table. A deployment claims its instance by
creating its directory (<oramaDir>/data/deployments/<namespace>-<name>),
which only one request can do, and records its namespace and name in a
.orama-owner file inside it. A deploy, update, rollback or replica that finds
the directory owned by another deployment is refused with 409; delete and
replica teardown leave another deployment's unit and files alone. A create that
fails before the deployment is recorded removes the directory again, and a
delete removes it (a delete whose files cannot be removed fails, so it can be
retried). An archive's own .orama-owner is never extracted.
A directory from before this check has no .orama-owner. It is adopted by the
deployment it is named after if the gateway handling the request has that
deployment, and refused otherwise — the files may be another gateway's, and
until an operator removes them the name cannot be used.
Replicas
A dynamic deployment (Next.js SSR, Node.js, Go) runs on its home node and on a
second node, its replica. After the home node pins the build artifact it calls
the replica node's internal POST /v1/internal/deployments/replica/setup. The
artifact is not always on that node yet when the call arrives, so the replica
polls for it (every 2 s, up to 60 s) instead of failing on the first miss, and
only then extracts it, starts the unit and answers its port. Only "not
retrievable yet" is waited for; any other fetch failure is answered at once.
The internal replica routes (setup, update, rollback, teardown, env) answer
every error as JSON, {"error": "<reason>"}, with the status that fits. The home
node carries that reason into its own error and log, so a failed setup reads
node <id> returned status 500: Failed to extract content: ... rather than a
parse error.
A setup that fails is not dropped. The replica's row in deployment_replicas is
written with status failed and a replica_setup_failed event carrying the
reason is added to the deployment's events (GET /v1/deployments/events). The
cluster leader's reconciliation, every 5 minutes, sees the deployment
under-replicated and sets a replica up again, with the deployment's stored
(sealed) environment decoded first. A deployment whose environment cannot be
read is not re-replicated, and the failure is logged, rather than started
without its variables.
An update (orama deploy ... --update) and a rollback are applied on the
home node, then on every active replica, and the command waits for each (the
response's deadline is moved to cover one replica call, 180 s plus 30 s). It
succeeds only when all replicas applied it. A replica that did not is reported
with status 502, naming how many replicas applied it and each node that did not
with a generic reason (refused with a status, did not answer in time, or could
not be reached; the peer's own text and overlay addresses go to the home node's
log, not to the response), and a replica_update_failed event is
added to the deployment's events; the home node already runs the new version,
and the failed nodes keep serving the old one. Running the same update or
rollback again retries: it makes a new version and sends it to every replica.
Each call carries the deployment's version; a replica refuses (409) a version
older than the one it applied, so an older update that arrives late cannot put it
back. Update, rollback and environment changes of one deployment take turns on
the home node and on the replica, and a replica's setup and teardown take the same lock.
The waits nest, each strictly shorter than the one around it, and an update
or rollback has two segments so a slow local step cannot starve the replicas: the
lock wait and the work on the home node (extract, pin, start, health) share
120 s, then the replicas get their own 210 s (one replica call is bounded at
180 s, with room to report). The home node's whole handler is therefore bounded
at 330 s; the gateway hop to the home node allows 360 s and the entry gateway's
write deadline is moved to 390 s, for the routes that change a deployment
(update, rollback, environment, delete). The home node moves its own deadlines
before it reads the upload or waits for the lock. An update or rollback that
finds the lock held past its 120 s is refused with 503 (run it again).
Update, rollback and environment change are made on the home node only, because
the lock and the version stamps live there. One that reaches another gateway is
forwarded; if the home node does not answer it is refused with 503 and
Retry-After, and never run on the node that took the request. The marker
header that says a request was already forwarded (X-Orama-Proxy-Node) is
honoured only from a peer on the WireGuard overlay: the entry gateway removes it
from every other request, so a client cannot use it to run a change on any node.
Delete is not tied to the home node: it carries no version stamp, takes the
deployment lock on whichever node runs it, and falls through to that node when
the home node cannot be reached, so a deployment whose home node is gone for
good can still be deleted (by name or by id; both are routed the same way).
An environment change that finds an update holding the lock waits at most its
own 30 s local budget, then is refused with 503 (run it again). (Deleting a deployment still tells its replicas
to tear down without waiting; that is not an update.)
The node's health checker probes each local replica's own port every 30 s. A
replica row with no port (a failed setup) is not probed, and a failed replica
is marked active again only when the probe answers and its own systemd
unit reports active; an answer from some other process on the port does not
bring it back.
The same checker reaps orphan runtime units: an orama-deploy-{node,npm,go}@<instance>
unit that outlived its deployment (a delete whose stop was refused, or that ran
while the node was down) and still holds the port the registry has freed. Every
2 minutes it lists the node's runtime units (any state; the build@ and
clean@ oneshots are ignored) and compares their instances with every row of
the registry's deployments table, in any status. A unit with no row is stopped,
disabled, and its staged secrets, installed dependencies and extracted files
are removed (a unit whose stop fails keeps its files, which it still runs from),
and a Warn line names it. Every doubt stops nothing: the registry read must
succeed and hold at least one row; the unit must be unmatched on two consecutive
sweeps; its age must be at least 10 minutes, the longer of its systemd age (last
activation, or last state change if failed) and how long this node has been
finding it unmatched, so a crash-looping unit whose activation keeps resetting
is still reaped; and at most 2 units are stopped per sweep. A sweep runs on its
own timer, bounded to 90 seconds, so it never delays the replica probes. A unit
that gains a row, disappears, or is seen while the registry read fails or is
empty starts over. A stop that fails is logged as an error and retried on a
later sweep.
Deploying Next.js Applications
Deploy Next.js apps with full SSR (Server-Side Rendering) support.
Prerequisites
⚠️ IMPORTANT: Your
next.config.jsMUST haveoutput: 'standalone'for SSR deployments.
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone', // REQUIRED for SSR deployments
}
module.exports = nextConfigThis setting makes Next.js create a standalone build in .next/standalone/ that can run without node_modules.
Next.js with SSR
# 1. Ensure next.config.js has output: 'standalone' # 2. Deploy the project source directory with SSR enabled # (the CLI installs dependencies, runs the build, and uploads for you) cd my-nextjs-app orama deploy nextjs . --name my-nextjs --ssr # Output: # 📦 Installing dependencies... # 🔨 Building Next.js application... # 📦 Creating tarball from standalone output... # ☁️ Uploading to Orama Network... # # ✅ Deployment successful! # # Name: my-nextjs # Type: nextjs # Status: active # Version: 1 # Port: 10200 # # URLs: # • https://my-nextjs.orama.network # # ⚠️ Note: SSR deployment may take a minute to start. Check status with: orama app get my-nextjs
What Happens Behind the Scenes
- +Build: The CLI runs
npm install(ifnode_modulesis missing) andnpm run buildin your project directory - +Tarball Upload: The
.next/standalone/output (with.next/staticandpubliccopied in) is tarballed and uploaded - +Home Node Assignment: A node is chosen to host your app based on capacity
- +Port Allocation: A unique port (10200-19999) is assigned
- +Systemd Service: A systemd service is created to run
node server.js - +Health Checks: Gateway monitors your app every 30 seconds
- +Reverse Proxy: Gateway proxies requests from your domain to the local port
Static Next.js Export (No SSR)
If you export Next.js to static HTML:
# next.config.js
module.exports = {
output: 'export'
}
# Build and deploy as static
npm run build
orama deploy static ./out --name my-nextjs-staticDeploying Go Backends
Deploy Go backends for high-performance APIs.
Prerequisites
⚠️ IMPORTANT: Your Go application MUST:
- +Have a
go.modin the project root (the CLI cross-compiles for you)- +Listen on the port from
PORTenvironment variable- +Implement a
/healthendpoint that returns HTTP 200 when ready- +Build with
CGO_ENABLED=0(the CLI compiles with cgo disabled — use pure-Go dependencies)
Go REST API Example
# Deploy the project source directory — the CLI cross-compiles # for linux/amd64 (CGO_ENABLED=0) and uploads for you cd my-go-api orama deploy go . --name my-api # Output: # 🔨 Building Go binary (linux/amd64)... # 📦 Creating tarball... # ☁️ Uploading to Orama Network... # # ✅ Deployment successful! # # Name: my-api # Type: go # Status: active # Version: 1 # Port: 10201 # # URLs: # • https://my-api.orama.network
Example Go API Code
// main.go
package main
import (
"encoding/json"
"log"
"net/http"
"os"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
})
http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
users := []map[string]interface{}{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
}
json.NewEncoder(w).Encode(users)
})
log.Printf("Starting server on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}Important Notes
- +Environment Variables: The
PORTenvironment variable is automatically set to your allocated port - +Health Endpoint: REQUIRED - Must implement
/healththat returns HTTP 200 when ready - +Automatic Cross-Compilation: The CLI runs
go build -o app .withGOOS=linux GOARCH=amd64 CGO_ENABLED=0— no manual build needed - +No cgo: Because builds use
CGO_ENABLED=0, dependencies requiring cgo (e.g.mattn/go-sqlite3) will not work — use pure-Go alternatives (e.g.modernc.org/sqlite) - +Systemd Managed: Runs as a systemd service with auto-restart on failure
- +Port Range: Allocated ports are in the range 10200-19999. 10000-10199 is the tenant and index blocks (rqlite, Olric, the gateway, IPFS) and is not given to an app.
Deploying Node.js Backends
Deploy Node.js/Express/TypeScript backends. Nodes run them on Node.js 24 LTS (24.21.0), which every node installs; there is no per-deployment choice of version.
Prerequisites
⚠️ IMPORTANT: Your Node.js application MUST:
- +Listen on the port from
PORTenvironment variable- +Implement a
/healthendpoint that returns HTTP 200 when ready- +Have a valid
package.jsonwith either:
- +A
startscript (runs vianpm start), OR- +A
mainfield pointing to entry file (runs vianode {main}), OR- +An
index.jsfile (default fallback)
Express API Example
# Deploy the project source directory — the CLI installs dependencies, # runs the build script (if any), and uploads for you cd my-node-api orama deploy nodejs . --name my-node-api # Output: # 📦 Installing dependencies... # 🔨 Building... # 📦 Creating tarball... # ☁️ Uploading to Orama Network... # # ✅ Deployment successful! # # Name: my-node-api # Type: nodejs # Status: active # Version: 1 # Port: 10202 # # URLs: # • https://my-node-api.orama.network
Example Node.js API
// server.js
const express = require('express');
const app = express();
const port = process.env.PORT || 8080;
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.get('/api/data', (req, res) => {
res.json({ message: 'Hello from Orama Network!' });
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});Important Notes
- +Environment Variables: The
PORTenvironment variable is automatically set to your allocated port - +Health Endpoint: REQUIRED - Must implement
/healththat returns HTTP 200 when ready - +Dependencies: The CLI runs
npm install --productionlocally ifnode_modulesis missing;node_modulesand hidden files are excluded from the uploaded tarball, and dependencies are installed on the server — unless the tarball carries its ownnode_modulesor the upload sets theskip_install=trueform field (API only) - +How the server installs them: in a sandboxed one-shot unit,
orama-deploy-build@<namespace>-<name>, asnpm install --omit=dev --ignore-scripts --no-audit --no-fundrun against copies of yourpackage.jsonandpackage-lock.json/npm-shrinkwrap.jsononly. That means:- +No install scripts run — not yours, not any dependency's (
preinstall,install,postinstall,prepare). Packages that compile or download a native binary in a script (bcrypt,sharp,sqlite3, …) will not work from a server install: shipnode_modulesbuilt for Linux x86-64 in the tarball instead.orama deploy nodejsleavesnode_modulesout of the tarball, so shipping it means uploading the tarball through the API (POST /v1/deployments/nodejs/upload) - +Only npm registry dependencies are installed. A git dependency (
github:user/repo,user/repo,git+ssh://…), a URL,file:,link:or a path (../lib) — in any dependency field,overridesor the lockfile — andworkspacesare refused before the install starts, with500and a message naming the dependency. Shipnode_modulesfor those - +Your
.npmrcis not read, so a private registry or auth token configured there is not used; the build reaches the public npm registry only (no loopback, no private networks), and a lockfile's tarball URLs are fetched fromregistry.npmjs.orgwhatever host they name. Shipnode_modulesfor private packages - +The install must finish within 4 minutes; a failed install fails the deploy, and npm's output is in
journalctl -u orama-deploy-build@<namespace>-<name>on the node - +The installed
node_modulesis mounted one directory above your app, so Node finds it after anynode_modulesyour app ships itself. It is removed when the deployment is deleted, and a deployment created without a server-side install starts with none - +
npm run buildis not run on the server; build before uploading
- +No install scripts run — not yours, not any dependency's (
- +Start Command Detection:
- +If
package.jsonhasscripts.start→ runsnpm start - +Else if
package.jsonhasmainfield → runsnode {main} - +Else → runs
node index.js
- +If
- +Systemd Managed: Runs as a systemd service with auto-restart on failure
Environment Variables
Go, Node.js and Next.js SSR deployments run as a process, and that process reads its configuration from environment variables. Set them at deploy time, or change them afterwards without redeploying.
At deploy time
orama deploy go ./my-api --name my-api \ --env DATABASE_URL=postgres://... \ --env LOG_LEVEL=debug # Or read them from a file orama deploy go ./my-api --name my-api --env-file .env.production
--env is repeatable and splits on the first = only, so a value may contain
one. --env-file reads a .env: KEY=VALUE per line, # comments and blank
lines skipped, one layer of surrounding quotes removed. It does not expand
$VAR — the literal text in the file is what gets sent, not whatever the
machine running the deploy happens to have set. A --env on the command line
overrides the same name from the file.
--env and --env-file belong to a first deploy. With --update the CLI
refuses them: an update replaces the build and never reads variables, so they
would be dropped without a word. Change variables on a running app with
orama app env set, below.
After deploying
orama app env list my-api orama app env set my-api --env DATABASE_URL=postgres://... orama app env set my-api --env-file .env.production orama app env unset my-api OLD_FLAG DEBUG_MODE
Setting or removing a variable rewrites the app's environment file and restarts it, so the change takes effect immediately. A static site has no process, so its variables are recorded and nothing is restarted.
The change reaches every node that runs the app, not only the home node, and the command succeeds only when all of them applied it.
- +The home node takes the deployment's lock (an environment change, an update and a rollback of one deployment take turns), reads the deployment inside it, and writes the new environment with a compare-and-swap on the stored value; a write that finds the row moved is refused (409, run the command again).
- +It restarts its own unit, then calls each active replica's internal
POST /v1/internal/deployments/replica/envin parallel and waits for every answer. The environment goes sealed with the cluster key, as in a replica setup, with a version stamp (a nanosecond timestamp made strictly increasing per deployment). The stamp is also written as the row'supdated_at, so after a restart with the clock behind, the next stamp starts above the previous one and no replica refuses it for good (409). - +A replica takes the deployment's type, limits and port from its own registry row, found by deployment id, namespace and name together on this node's active replica; a request that pairs an id with another deployment's name is refused (404), and so is a deployment with no process (400). It refuses a version older than the one it already applied (409; the applied version is a file beside the deployment's directory, so it survives a restart), re-writes its env file, re-mints the workload token and restarts the unit.
- +If a replica could not be reached or refused, the command fails with status 502
and a message naming how many replicas applied the change and each node that did
not, with a generic reason (never the peer's own text or its address). A
replica_env_failedevent is added to the deployment's events. The change is already saved and applied on the home node and on the replicas that answered; the failed nodes keep the old environment until the command is run again. Retry is the same command, which re-sends the environment to every replica (applying an environment is idempotent); there is no retry of only the failed nodes.
The waits nest, each strictly shorter than the one around it, so the 502 reaches
the user instead of a timeout: a replica's restart is bounded at 20 s, the home
node waits at most 30 s for one replica, the whole change (lock wait included)
is bounded at 60 s, and orama app env set / unset wait 90 s (every other CLI
call keeps its 30 s). The gateway's own limits sit outside all of them: the
hop to the home node for a deployment change allows 360 s and the entry
gateway's write deadline is 390 s. The environment change's 60 s is split the same
way: 30 s for the lock wait, the write and the local restart, then 30 s for the
replicas whatever the local step took.
The whole fleet must run the version that has this endpoint. A replica that
does not know /v1/internal/deployments/replica/env answers 404, so during a
rolling upgrade an environment change on an app with a not-yet-upgraded replica
fails with 502 naming that node (refused the change (status 404)). The change is
saved and applied on the home node; run the command again once that node is
upgraded.
list shows names, never values. Environment variables are where secrets
live, so an endpoint that echoed them would put every secret behind nothing more
than a read scope, and into whatever terminal scrollback or CI log the caller is
writing to. To change a value, set it again.
What the platform sets for you
These names are set by the platform and cannot be overwritten or removed:
| Name | What it is |
|---|---|
PORT | The port your app must listen on, over TCP. It is how the gateway reaches you, and the only port your app may bind: listening on any other port, or binding a UDP socket, fails with EPERM. Outbound connections need no bind and are unaffected |
ENTRY_POINT | What a Node.js deployment runs |
ORAMA_ENTRYPOINT | The script the runtime starts, derived from ENTRY_POINT |
ORAMA_NAMESPACE | The namespace this deployment belongs to |
ORAMA_GATEWAY_URL | Your namespace's own gateway, https://ns-<namespace>.<domain> |
ORAMA_STATE_DIR | A directory your app may write to that survives restarts |
ORAMA_CACHE_DIR | A directory your app may write to that it must not rely on |
An app that talks back to Orama previously had nothing to go on: no address, no namespace name. Every such app baked both into its own image.
Your app's own credential
Your app is a principal of its own. At start it is handed a short-lived token in
the file named by $ORAMA_TOKEN_FILE, and it renews that token with the gateway
before it expires:
const token = await readFile(process.env.ORAMA_TOKEN_FILE, "utf8");
// Before it expires, ask for the next one with the one you have.
const res = await fetch(`${process.env.ORAMA_GATEWAY_URL}/v1/auth/renew`, {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
});
const { access_token, expires_in } = await res.json();Nothing long-lived is on the node. systemd reads the file as PID 1 and hands your process a copy owned by your app's own user; the gateway never has to write anything your app could read directly.
It reaches nothing until you grant it something, which is the point — an app that ships with no credential cannot leak one:
orama app grants set my-api runtime # invoke, storage, push, webrtc, proxy, pubsub, cache orama app grants list
A deployment cannot be granted the control plane. If something needs to deploy or mint keys, that is a person or a CI key, not an app.
A grant you change reaches a running app on its next renewal, or immediately if you redeploy.
Where your app may write
Your app's own directory is read-only. The files there are its build output, and
a process that can rewrite its own code cannot be rolled back to a known
version. Write to $ORAMA_STATE_DIR instead, which is yours alone and survives
restarts, or $ORAMA_CACHE_DIR for anything you can regenerate.
How the values are handled
Values are held encrypted in the cluster database, with a key derived from the
cluster secret, and sealed to the deployment's namespace and id, so a copy of
the stored value in another deployment's row does not open (on a cluster
upgraded from an earlier release this starts when an operator runs
orama maint operator rotate-secrets; a new cluster has it from the start).
On the node they are written to a file only the system can read,
which systemd hands to your process — they are not written into the app's
systemd unit, and they are removed from the node when the deployment stops.
A value may contain anything that is valid UTF-8, including quotes,
backslashes, spaces and newlines, so a PEM key or a JSON blob goes in as it is.
It may not contain a NUL byte, and one value may be at most 64 KiB: every value
is replicated to every node in the cluster.
The environment as a whole, rendered as the file your app's unit reads, may be
at most 224 KiB; a deploy or env set that would exceed it is refused with
400. (The node stages at most 256 KiB, and the platform adds its own
variables.)
What runs your app
Your app is an instance of a systemd template installed with the platform —
orama-deploy-node@, orama-deploy-npm@ or orama-deploy-go@ — named
orama-deploy-<runtime>@<namespace>-<name>. Which one you get follows from the
deployment type and, for Node.js, from ENTRY_POINT: npm:start runs
npm start, anything else runs node <that file>, and no value runs
node index.js.
The gateway does not write a unit for your app. It writes only the environment
file, and starts the template. That is not an implementation detail you can
ignore if you are running a node: the gateway runs as an unprivileged user with
ProtectSystem=strict and NoNewPrivileges=yes, so it cannot write into
/etc, and a node whose templates were not installed will refuse every deploy
with "Unit orama-deploy-node@… not found". They are installed by
orama maint node install and by every upgrade.
What your app runs as
Each deployment runs as its own unprivileged user, allocated for it and reclaimed when it stops, so no two deployments share an identity and none of them is root. It cannot reach the cluster's internal network, and it has the memory, CPU and process limits recorded on the deployment.
Of the platform's own directory tree it sees only its own deployment
directory, read-only: not the node's configuration or secrets, and not any
other deployment or database. Files it creates in ORAMA_STATE_DIR and
ORAMA_CACHE_DIR are private to it (UMask=0077).
Health check path
orama deploy go ./my-api --name my-api --health-check /healthz
The platform polls this path to decide the app has started. It defaults to
/health.
Why there is no orama.yaml
Repeated deploys still retype --name. A project file holding the name, type,
environment and health-check path would remove that, and it is worth doing — but
it is a design commitment, not a convenience: it needs precedence rules against
flags, a version field, validation, and an answer for what a checked-in file
does with secrets. --env-file already covers the part that hurt most. The
project file is tracked separately rather than half-built here.
Full-Stack Application Example
Deploy a complete full-stack application with React frontend, Go backend, and SQLite database.
Architecture
┌─────────────────────────────────────────────┐
│ React Frontend (Static) │
│ Domain: myapp.orama.network │
│ Deployed to IPFS │
└─────────────────┬───────────────────────────┘
│
│ API Calls
▼
┌─────────────────────────────────────────────┐
│ Go Backend (Dynamic) │
│ Domain: myapp-api.orama.network │
│ Port: 10200 │
│ Systemd Service │
└─────────────────┬───────────────────────────┘
│
│ SQL Queries
▼
┌─────────────────────────────────────────────┐
│ SQLite Database │
│ Name: myapp-db │
│ File: .../data/sqlite/ns/myapp-db.db │
└─────────────────────────────────────────────┘Step 1: Create the Database
# Create database
orama db create myapp-db
# Create schema
orama db query myapp-db "CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)"
# Insert test data
orama db query myapp-db "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com')"Step 2: Deploy Go Backend
Backend Code (main.go):
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"os"
_ "modernc.org/sqlite" // pure-Go driver (deployments build with CGO_ENABLED=0)
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
CreatedAt string `json:"created_at"`
}
var db *sql.DB
func main() {
// Orama only injects the PORT env var — the database path is up to you.
// Databases created with `orama db create` live on the home node at:
// /opt/orama/.orama/data/sqlite/{your-namespace}/{db-name}.db
dbPath := "/opt/orama/.orama/data/sqlite/your-namespace/myapp-db.db"
var err error
db, err = sql.Open("sqlite", dbPath)
if err != nil {
log.Fatal(err)
}
defer db.Close()
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// CORS middleware
http.HandleFunc("/", corsMiddleware(routes))
log.Printf("Starting server on port %s", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
func routes(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/health":
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
case "/api/users":
if r.Method == "GET" {
getUsers(w, r)
} else if r.Method == "POST" {
createUser(w, r)
}
default:
http.NotFound(w, r)
}
}
func getUsers(w http.ResponseWriter, r *http.Request) {
rows, err := db.Query("SELECT id, name, email, created_at FROM users ORDER BY id")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer rows.Close()
var users []User
for rows.Next() {
var u User
rows.Scan(&u.ID, &u.Name, &u.Email, &u.CreatedAt)
users = append(users, u)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
result, err := db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", u.Name, u.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
id, _ := result.LastInsertId()
u.ID = int(id)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next(w, r)
}
}Deploy Backend:
# Deploy the source directory — the CLI cross-compiles # for linux/amd64 (CGO_ENABLED=0) and uploads for you orama deploy go . --name myapp-api
Step 3: Deploy React Frontend
Frontend Code (src/App.jsx):
import { useEffect, useState } from 'react';
function App() {
const [users, setUsers] = useState([]);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const API_URL = 'https://myapp-api.orama.network';
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => {
const response = await fetch(`${API_URL}/api/users`);
const data = await response.json();
setUsers(data);
};
const addUser = async (e) => {
e.preventDefault();
await fetch(`${API_URL}/api/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, email }),
});
setName('');
setEmail('');
fetchUsers();
};
return (
<div>
<h1>Orama Network Full-Stack App</h1>
<h2>Add User</h2>
<form onSubmit={addUser}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name"
required
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
type="email"
required
/>
<button type="submit">Add User</button>
</form>
<h2>Users</h2>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
);
}
export default App;Deploy Frontend:
# Build npm run build # Deploy orama deploy static ./dist --name myapp
Step 4: Access Your App
Open your browser to:
- +Frontend:
https://myapp.orama.network - +Backend API:
https://myapp-api.orama.network/api/users
Full-Stack Summary
✅ Frontend: React app served from IPFS ✅ Backend: Go API running on allocated port ✅ Database: SQLite database with ACID transactions ✅ Domains: Automatic DNS for both services ✅ Isolated: All resources namespaced and secure
Managing Deployments
List All Deployments
orama app list # Output: # NAME TYPE STATUS VERSION CREATED # my-react-app static active 1 2024-01-22 10:30 # myapp-api go active 1 2024-01-22 10:45 # my-nextjs nextjs active 2 2024-01-22 11:00 # # Total: 3
Get Deployment Details
orama app get my-react-app # Output: # Deployment: my-react-app # # ID: dep-abc123 # Type: static # Status: active # Version: 1 # Namespace: your-namespace # Content CID: QmXxxx... # Memory Limit: 256 MB # CPU Limit: 50% # Restart Policy: always # # URLs: # • https://my-react-app.orama.network # # Created: 2024-01-22T10:30:00Z # Updated: 2024-01-22T10:30:00Z
View Logs
# View last 100 lines orama app logs my-nextjs # The last 500 lines (1 to 1000; --follow is not supported) orama app logs my-nextjs --lines 500
Rollback to Previous Version
# Rollback to version 1 orama app rollback my-nextjs --version 1 # Output: # ⚠️ Rolling back 'my-nextjs' to version 1. Continue? (y/N): y # # ✅ Rollback successful! # # Deployment: my-nextjs # Current Version: 1 # Rolled Back From: 2 # Rolled Back To: 1 # Status: active
Delete Deployment
orama app delete my-old-app # Output: # ⚠️ Are you sure you want to delete deployment 'my-old-app'? (y/N): y # # ✅ Deployment 'my-old-app' deleted successfully
Troubleshooting
Deployment Issues
Problem: Deployment status is "failed"
# Check deployment details orama app get my-app # View logs for errors orama app logs my-app # Common issues: # - App not listening on the PORT environment variable # - Missing dependencies (not declared in package.json / go.mod) # - Port already in use (shouldn't happen, but check logs) # - bind: operation not permitted — the app listens on a port other than PORT, # or binds UDP; a deployment may bind only PORT, over TCP # - Health check failing (ensure /health endpoint exists)
Problem: Can't access deployment URL
# 1. Check deployment status orama app get my-app # 2. Verify DNS (may take up to 10 seconds to propagate) dig my-app.orama.network # 3. For local development, add to /etc/hosts echo "127.0.0.1 my-app.orama.network" | sudo tee -a /etc/hosts # 4. Test with Host header curl -H "Host: my-app.orama.network" http://localhost:10104/
Database Issues
Problem: Database not found
# List all databases orama db list # Ensure database name matches exactly (case-sensitive) # Databases are namespace-isolated
Problem: SQL query fails
# Check table exists orama db query my-db "SELECT name FROM sqlite_master WHERE type='table'" # Check table schema (sqlite3 dot-commands like .schema are NOT supported — plain SQL only) orama db query my-db "SELECT sql FROM sqlite_master WHERE name='users'"
Authentication Issues
# Re-authenticate. logout ends the session on the gateway as well as here. orama auth logout orama auth login # Ask the gateway whether this credential still works, and what it holds orama auth whoami
Need Help?
- +Documentation: Check
/docsdirectory - +Logs:
orama app logs <app>for an application. The gateway's own log is at~/.orama/logs/gateway.logon a node, not on your machine — reach it withorama node logs. - +Issues: Report bugs at GitHub repository
- +Community: Join our Discord/Telegram
Best Practices
Security
- +Never commit sensitive data: Keep secrets in environment variables, set with
orama app env set --env-filerather than a flag so they stay out of shell history. See Environment Variables - +Validate inputs: Always sanitize user input in your backend
- +HTTPS only: All deployments automatically use HTTPS in production
- +CORS: Configure CORS appropriately for your API
Performance
- +Optimize builds: Minimize bundle sizes (React, Next.js)
- +Use caching: Leverage browser caching for static assets
- +Database indexes: Add indexes to frequently queried columns
- +Health checks: Implement
/healthendpoint for monitoring
Deployment Workflow
- +Test locally first: Ensure your app works before deploying
- +Use version control: Track changes in Git
- +Incremental updates: Use
--updateflag instead of delete + redeploy - +Backup databases: Regular backups via
orama db backup - +Monitor logs: Check logs after deployment for errors
Databases, domains and WebRTC
SQLite databases are on Databases, custom domains and routing on Domains and Routing, and WebRTC for a namespace on WebRTC.
Programmatic Deployments
For CI/CD pipelines, use the CLI directly. Deployments are managed via the REST API:
# In CI/CD, set credentials via environment variable export ORAMA_TOKEN="$CI_ORAMA_KEY" # an admin key kept in your CI secret store # Deploy orama deploy static ./dist --name my-app --update
The gateway exposes deployment REST endpoints at /v1/deployments/{type}/upload and /v1/deployments/{type}/update that accept multipart file uploads with authentication.
Next Steps
- +Every command and flag: CLI reference, generated from the command tree
- +Every gateway route: API surface, with which client owns each one
- +Custom domains:
orama domain add|verify|list|remove, and Domains and Routing - +Production Deployment: Install nodes with
orama maint node installfor production clusters - +From code: the TypeScript SDK or the Go client