---
name: egbe-os-deploy
description: Publish, update and diagnose apps on Egbe hosting, the default for connected businesses.
metadata:
  egbe-when: "Use for website/app previews, publication, updates or deployment diagnosis."
  egbe-visible: "true"
  egbe-title: "Hosting & databases"
  egbe-description: "Publish your application, attach a database and manage updates through Egbe hosting."
  egbe-category: "Development & hosting"
---

<!-- egbe-catalog-updates:v1 -->
For work in a workspace with an installed, executable Egbe catalog, run
`python3 "<skills-directory>/egbe-os-quickstart/scripts/sync-catalog.py" --check` using the
actual installation path before starting work with this skill. `<skills-directory>` is the
project folder that contains this skill's directory, such as `.agents/skills`; find it with
file tools or `ls`, not inline interpreter code. Follow its result
with `<skills-directory>/egbe-os-quickstart/references/skill-updates.md`, then reread this
skill if updated. The check is read-only; the reference covers applying updates.
For public guidance without an installed catalog or command execution, skip
this local check and read the current published instructions directly. Reading
does not require a project or installation. For project execution that needs
connection or setup, use `GET /quickstart` on `OS_URL` to choose the appropriate
workspace or no-installation path before setup. The quickstart handles its own check.

Scheduled and other unattended runs have no one to approve inline code, so
`python -c`, `node -e`, heredoc scripts and code-execution tools may be blocked
there. Run bundled scripts as `python3 <script> ...` or `node <script> ...`, and
read files with file tools or plain shell commands.

<!-- egbe-connection-identity:v1 -->
Public instruction and playbook reads do not require credentials or connection
identity. The following applies to authenticated project requests, not reading
or applying public guidance without a connection.

For a BYOA workspace, retain its connection identity on **every authenticated
Egbe request**, including the examples and referenced workflows below. Load
`OS_URL` and `OS_API_KEY` from the project's Git-ignored `.env.local`. Set
`OS_AGENT_NAME` to your actual human-readable agent name, for example
`Claude Code` or `Codex`, not a slug such as `claude-code` or `CLAUDE_CODE`.
This is the name the owner sees in Dashboard. Never infer it from curl or reuse
another agent's identity. If your name is unknown, leave it unset. Quickstart saves the per-agent UUID in
`OS_CONNECTION_ID_<AGENT_SLUG>` (uppercase name with separators changed to
underscores, e.g. `CLAUDE_CODE`) and the optional owner-provided nickname in
`OS_WORKSPACE_NAME` in that file.

Many agent runtimes, including Claude Code, start a fresh shell for every
command, so shell functions and exported variables from an earlier command are
gone. Make each command that calls Egbe self-contained: load `OS_URL` and
`OS_API_KEY` in that same command, and run each authenticated curl example
through the project-local helper with your agent name set inline:

```bash
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request /me/limits -s
```

`--egbe-request` loads the saved URL and API key from `.env.local` without
executing it, constructs the full URL, and adds the bearer token before calling
curl. Replace `/me/limits` with the API path from an example and keep its other
curl options; omit the example's URL and Authorization header. This mode rejects
additional destinations and redirects. Without this mode, the helper takes the
same arguments as `curl` and you must load credentials before shell expansion.
Do not edit global shell
configuration. Only in a shell that persists between your commands may you
instead export `OS_AGENT_NAME` once and define
`curl() { python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" "$@"; }`.
Likewise prefix `OS_AGENT_NAME="<your agent name>"` on each command that runs a
bundled Python or JavaScript client.

The helper adds the saved identity headers only for the configured Egbe URL;
other destinations receive no extra headers. For an Egbe request it exits 22 on
an HTTP status of 400 or above and writes a status and request ID line to stderr
after every response; `<skills-directory>/egbe-os-quickstart/references/connection.md`
describes both. It rejects mixed destinations and redirects when attaching
identity. If the helper or this agent's UUID is missing, follow `GET /quickstart`
first. Before catalog installation, download and inspect
`GET /skills/quickstart/resources/scripts/egbe-curl.py` into the workspace's temporary
setup directory and use that path. After installation, switch to the installed helper.
For other Python, JavaScript or MCP clients, explicitly configure the same
`X-Egbe-Connection-Id`, `X-Egbe-Agent-Name`, and optional `X-Egbe-Workspace-Name`
headers on Egbe requests. A client that cannot attach identity headers remains
usable as an unidentified connection; do not claim its later activity is identified.
Deployed-product requests use their separate product credentials and no agent identity.


# Deploy a web app

Egbe hosting is the default destination for authorized publishing in a connected
Egbe business. Another available tool, including ChatGPT Sites, is not a reason
to switch hosts. Follow an explicit founder choice of another host and preserve
an existing product's destination unless migration is authorized. Diagnose Egbe
failures and report concrete blockers rather than silently publishing elsewhere.

Before a first public release, ensure its product name and public identity were
selected by the founder or under explicit delegation. The first deploy also
claims a public alias from the slug. For unresolved personal choices or
hard-to-reverse changes such as replacing an established public identity or
destroying saved customer work, present options and a recommendation and wait for
selection or delegation. Return the decision to the caller if needed. Offer
delegation once or throughout this business, and reuse it without asking again;
routine authorized updates under existing decisions need no new checkpoint.

Give us a git repo; we build the `Dockerfile` and host it live. The live URL is
returned by `GET /deploy/<slug>` once the build finishes.

**Read `url`, not `host`.** Every response carries both: `host` is the canonical
`<slug>-<publicId>.n.egbe.app` address, and `url` is what you give a customer — your
`<name>.egbe.app` domain when you have one (your first deploy claims it for you) and the
canonical host otherwise. See the `domains` capability (`GET /domains/skill`) to choose
a name or move your one domain to another app.

Use the project's configured `OS_URL` and authenticate every call with
`Authorization: Bearer $OS_API_KEY`.

## App requirements
- **Listen on `0.0.0.0`** on the port your `Dockerfile` `EXPOSE`s (the runtime
  auto-detects it; default 8080). Don't bind to `127.0.0.1`.
- **Need a database? Pass `"db": true`** (see below) — do NOT try to reach an
  external Postgres host. Your app runs on an isolated network and can only
  reach a database provisioned for it on this platform.
- **Public product app? Use the `analytics` capability** to create/fetch the
  analytics site and inject `GET /analytics/<slug>/tracking` config into the app
  shell. The deploy skill does not define product event names; read
  `GET /analytics/events` for the canonical funnel contract.

## Saved customer work

For an app that promises to save customer work, a healthy deployment is not
proof of recovery. Before declaring that saved-work journey complete, exercise
its actual save and recovery path on the serving revision. Stateless landing
pages and read-only sites have no save requirement; do not invent one. Create meaningful work, change each important editable part (including
checkboxes, status, selections and quantities), save through the customer UI,
then reload and leave/return to the page. Compare the original values and
checked/unchecked state; recovering only the inputs is insufficient when the
saved result contains further customer edits.

Use storage appropriate to the promised lifetime. Explain browser-only saving
plainly; do not claim account or cross-device recovery from local browser data.
If the customer saves a file, verify its contents and reopen/import it through
the supported product journey instead. An exported checklist must reflect the
customer's actual choices. Do not claim a download works from its click alone.

Before updating an existing app, keep an original saved customer record or file
and verify it still recovers after the update. Preserve existing storage keys,
identities and values, or ship a migration. Cover any confirmed loss with a
regression test and recheck the actual customer UI. Report failures and observer
limits honestly; mocked integrations and HTTP 200 cannot establish recovery.

## Need a database

Pass `"db": true` to the deploy call. We provision a Postgres co-located with
your app and inject its connection string as `DATABASE_URL` into the app's
environment — your code just reads `process.env.DATABASE_URL`. No host, user, or
password to manage.

```bash
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy" -s -X POST  \
 -H 'Content-Type: application/json' \
  -d '{"slug":"my-app","repo":"my-app","db":true}'
```

Need the connection string yourself (e.g. to run migrations)? Provision/fetch it
directly:

```bash
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy/my-app/db" -s -X POST  # provision -> {databaseUrl,...}
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy/my-app/db" -s      # fetch existing
```

To inspect persisted data, export query results, or update/delete a record,
read `GET /db/skill`. It uses the same attached PostgreSQL database and your
existing `OS_API_KEY`; no app admin route or redeploy is required.
The database schema is shared by your account's apps.

### Product schema contract

Provisioning a database only creates the database and role; it does not create app tables.
Every DB-backed route must ship an idempotent schema initializer or migration with the product code.

- Run the schema initializer before the first query in production, either during
  startup or on the first request behind a concurrency-safe guard.
- Keep schema setup in the deployable product repo, not only in local notes or
  one-off terminal history.
- Let routes degrade gracefully when optional demo, activation, onboarding, or
  dashboard data is absent.
- Verify every DB-backed route listed in a product manifest `test_paths` or `activation_paths` after deploy.
- A deployed route that produces any unhandled server/runtime error is not
  complete. Examples include `relation "<table>" does not exist`, database
  query failures, and ORM or framework runtime errors.

## Filesystem constraints (read this — it is the #1 cause of containers that won't start)

Your container runs with a **read-only root filesystem** and as a **non-root
user** for security. The only writable locations are these in-memory `tmpfs`
mounts (everything written there is lost on restart):

| Path         | Size  | Use for                                  |
|--------------|-------|------------------------------------------|
| `/tmp`       | 64 MB | scratch files                            |
| `/var/tmp`   | 64 MB | scratch files                            |
| `/var/cache` | 128 MB| caches (nginx, build tools)              |
| `/run`       | 16 MB | pid/socket files                         |

Anything that tries to write **anywhere else** (e.g. SQLite in the app dir, a
framework writing `.next`/`.cache` at runtime, log files next to the binary)
will fail to start. Fixes:
- Point your app's cache/temp/log dirs at `/tmp` or `/var/cache` (env var or
  config).
- Pre-build everything at image-build time so nothing is written at runtime.
- Store real data in attached PostgreSQL, never on the container disk.

## Before declaring deployment blocked

A missing remote, a local `master` branch or absent Vercel/Netlify configuration
is normal setup. Read `/git/skill`, create/reuse the account's Forgejo repo and
push with native Git through `/git/http/<repo>.git` using the OS key. Preserve
current history and branch names. Prepare a root Dockerfile if missing and deploy
the full pushed SHA. Respect the founder's existing authorization and budget; report only
an observed redacted auth/provisioning/network/build error or a real inability to
supply the key to Git. Verify status, deployed revision and public URL after deploy.

## Before you push: .gitignore and .dockerignore

Exclude dependencies, build output and actual environment files in both
`.gitignore` and `.dockerignore`, for example:

```gitignore
node_modules/
.next/
dist/
**/.env
**/.env.*
!**/.env.example
!**/.env.sample
!**/.env.template
*.log
```

Example files must contain sanitized placeholders. Add `.git` to `.dockerignore`;
Docker does not inherit Git exclusions. Ignore rules do not remove tracked secrets:
if credentials were committed, remove them from tracked source and coordinate
rotation before publication. Let the Dockerfile install dependencies (`npm ci`).

Keep `/egbe-docs/**/.local/` Git-ignored. Add `/egbe-docs/` to
`.dockerignore` and exclude it from every non-Docker build/public-copy path.
For static sites, publish only the intended output directory, never the whole
project root.

## Deploy

```bash
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy" -s -X POST  \
 -H 'Content-Type: application/json' \
  -d '{"slug":"my-app","business_name":"MyApp","repo":"my-app","ref":"<full-pushed-commit-SHA>","env":{"NODE_ENV":"production"}}'
```

- `slug` — your app's name; the live URL is returned by `GET /deploy/<slug>`.
- `business_name` — include the chosen business name exactly as branded, preserving
  spelling, spaces, acronyms and capitalization (for example, `FirstSignal` with
  slug `firstsignal`). This saves the name for the Dashboard business dropdown,
  header and settings. It initializes an unnamed business and preserves any
  existing chosen name; use Dashboard settings to rename a business. Do not
  derive the display name from the lowercase URL slug. Omit it only when no
  business name has been chosen yet.
- `repo` — the git repo to deploy (your Forgejo repo).
- `ref` — branch, tag or commit (default `main`). For your configured Forgejo,
  an unambiguous 4–39-character commit abbreviation is expanded to its full SHA
  before building. Hex-shaped names prefer a branch, then a tag, then a commit
  abbreviation; use `refs/heads/<name>` or `refs/tags/<name>` to choose explicitly.
  External repositories use BuildKit's native branch/tag or full 40-character
  commit SHA format; abbreviated commits are not supported there.
- `env` — optional runtime environment patch: supplied keys replace their saved
  values; omitted keys are preserved. Omit `env` for a code-only redeploy. This
  is not a replacement map or a way to remove an existing key.
- `db` — `true` to provision a co-located Postgres and inject `DATABASE_URL` (see above).
- `min_instances` — `1` to keep one instance running and avoid scale-to-zero cold starts.
  The running instance consumes wallet resources continuously, including while idle. The
  default is `0`; include `min_instances: 1` on every redeploy that should stay warm.

Returns `202` with the deploy info; the app builds in the background.

### Build-time public configuration

`env` is applied to the running container, not to the Docker build. Frameworks
that inline public variables during compilation (such as Next.js
`NEXT_PUBLIC_*`) will not pick them up from runtime env alone. Fetch the public
configuration from its owning OS capability before building, then either place
those non-secret values in a versioned public config consumed by the build, or
serve them through an explicit runtime-config endpoint. Verify the resulting
browser requests on the serving revision. Agent keys and server credentials
must never enter that public config or the image build context.

For ad pixels, follow the relevant advertising skill's setup and event contract;
deploy does not install pixels or discover their IDs automatically.

Use `"db": true` when first creating a DB-backed app or when explicitly
repairing/provisioning its database attachment. On later redeploys, the platform
preserves the existing `DATABASE_URL` runtime env automatically. Do not fetch,
store, or pass `DATABASE_URL` yourself.

## Check status, list, logs

```bash
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy" -s   # list your apps
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy/my-app" -s   # status
OS_AGENT_NAME="<your agent name>" python3 "<skills-directory>/egbe-os-quickstart/scripts/egbe-curl.py" --egbe-request "/deploy/my-app/logs?lines=200" -s  # logs
```

`status` returns `{host, runtime, lastDeploy}`. To tell a healthy app from one
whose latest deploy silently failed, compare the two revision fields:

- `runtime.running` — the newest revision is Ready (also `healthStatus`).
- `runtime.serving` / `runtime.servingRevision` — whether *any* revision is
  answering traffic, and which one.
- `runtime.latestRevision` / `runtime.latestRevisionLive` — the most recently
  built revision and whether it is the one serving.
- `lastDeploy.outcome` — `succeeded` (newest revision is live), `not_live` (your
  newest build failed readiness, so an **older** revision still serves — the host
  returns 200 but it is stale), or `failed` (nothing is serving).

The `releases` history in deploy, list and status responses includes
`source_commit`: the full immutable Git ref submitted to BuildKit for that
release. It is `null` for historical releases and native branch/tag refs that
were not resolved to a full SHA. Deploy the full pushed SHA when verification
needs source correspondence. Match the release `id` to `lastDeploy.release`
and check the live revision outcome before associating it with a serving update;
history alone does not prove which revision serves. This is build-input
provenance, not a server-filesystem attestation or a substitute for deployed
behavior and configuration checks. Never infer missing provenance from the
current branch tip or backfill an old release.

So if `latestRevisionLive` is `false` / `outcome` is `not_live`, your last build
never went live even though the URL works — pull
`GET /deploy/<slug>/deploys/<deployId>` for the readiness failure and redeploy.

`logs` returns `{runtime, build, lines}` — `runtime` is the container's recent
stdout/stderr, `build` is the last build's output. Tail more with `?lines=N`
(1–1000, default 100). If the app scaled to zero, `runtime` says so — send it a
request to wake it, then retry.

Redeploy by calling `POST /deploy` again with the same `slug`.

### Business names with Convex

When publishing through a managed Convex backend, include the same exact name
in the optional JSON body of `POST /deploy/<slug>/convex`:

```json
{"business_name":"FirstSignal"}
```

The business name is saved independently of the URL slug. Calls without a body
continue to work. Existing chosen names are preserved.
