Embarko is inBeta

Capabilities

Everything Embarko can do, one entry each, in the order an agent needs it. Each entry says what it is, when you need it, how to use it, and what to know before you rely on it.

If this page and the live endpoint disagree, believe the endpoint. GET https://ship.embarko.ai/capabilities is generated by the platform, so it is always current. This page is written by hand and can fall behind.

  • It is a contents page. Open the one topic you need, such as /capabilities/custom-domain, for the full answer.
  • It needs no token and is cached for an hour. Check it before you rely on a feature.
  • Each entry has a via field that names who can use it:
{
  "runtime": { "port_env": "PORT", "bind_address": "0.0.0.0", "dockerfile": "rejected" },
  "persistence": { "data_dir_env": "DATA_DIR", "survives_redeploy": true, "survives_node_loss": false },
  "features": {
    "deploy":         { "status": "available", "via": ["agent", "dashboard"] },
    "env_vars":       { "status": "available", "via": ["agent", "dashboard"] },
    "rollback":       { "status": "available", "via": ["agent", "dashboard"] },
    "custom_domains": { "status": "available", "via": ["agent", "dashboard"] },
    "analytics":      { "status": "available", "via": ["agent", "dashboard"] },
    "rename_app":     { "status": "available", "via": ["agent", "dashboard"] },
    "change_app_address": { "status": "available", "via": ["agent", "dashboard"] },
    "delete_app":     { "status": "available", "via": ["agent", "dashboard"] },
    "backups":        { "status": "unavailable" }
  }
}

Entries marked NOT AVAILABLE YET are planned but not there. An app that needs one today cannot use it. Each entry says what to do instead.

An agent with a deploy token can do almost everything here, including custom domains, analytics, renaming and deletion. One step it cannot do: create the DNS record for a custom domain. Only the domain's owner can, at their own registrar.

Everything is on ship.embarko.ai. The path does not tell you the auth rule:

AuthCalls
None, everGET /capabilities, POST /api/public/… (rate limited)
None while unclaimed, token once claimeddeploy, status, logs, agent feedback
Deploy token, alwaysdomains, analytics, rename, change web address, delete, env vars, deployment history, rollback, showcase

Deploy, status, logs, domains, analytics, rename and delete sit under /apps/<name>. Env vars, history, rollback and showcase sit under /api/apps/<name>. Copy each path from its own entry. Don't guess one.


Deploy application

Upload an app's source. Embarko builds it, runs it and serves it at a live HTTPS URL.

When you need it: The user asks to deploy, ship, publish, release or "put online" an app you built.

How:

tar -czf /tmp/app.tar.gz --exclude=node_modules --exclude=.git \
  --exclude='.env*' -C /path/to/app .

curl -X POST "https://ship.embarko.ai/apps" \
  -H "X-App-Name: my-app" \
  -H "X-App-Version: $(git rev-parse --short HEAD)" \
  -H "X-App-Type: portfolio" \
  -H "X-Agent-Name: claude" \
  -F "source=@/tmp/app.tar.gz"
  • A first deploy needs no Authorization header.
  • X-App-Type is optional. It says what the app is for (personal website, portfolio, task management, feedback system...). Set it if you know. If not, ask the user once. If they skip it, leave it out.
  • X-Agent-Name is your own name as an agent (claude-code, claude, chatgpt...). Always send it.

Full parameter and error reference: API contract.

Good to know:

  • 202 means accepted, not live. Poll links.status until status is live before you report anything. Follow the next field in each reply.
  • A static site usually answers 200 and live straight away. See Static sites.
  • One deploy is one app.
  • The manifest or index.html must be at the archive root.
  • No Dockerfile and no custom build pipeline.
  • Never package secrets.
  • A JSON upload is capped at 25MB and 2000 files. Use a .tar.gz for anything bigger.

Redeploy / update application

Ship a new version of an app that already exists.

When you need it: Any change to an app that is already live.

How: Make the same call as Deploy, with the same X-App-Name and a new X-App-Version. There is no separate update endpoint and no "create then deploy" step. If the app is claimed, send the deploy token. An anonymous deploy cannot update a claimed app.

Good to know:

  • The name decides which app you update. A new name quietly creates a second app on a second subdomain.
  • Env vars, custom domains and DATA_DIR data carry over. Don't resend them.
  • A redeploy starts a fresh container. Anything written outside DATA_DIR is lost.
  • Each redeploy is a new history entry. It is safe to repeat, but not free.

Static sites

An upload that is just files is published as is, with no build and no server process. That means an index.html and its assets, a public/ folder, or a Staticfile.

When you need it: It is detected on every deploy. The reply has app.kind: "static", and status is usually already "live" because the request waits for the publish. A single-page app that needs a build step (Vite, Create React App and similar) is still built and run as a server app.

How: Deploy it like any other app.

  1. The reply is normally 200 with status: "live" and the next step (claim it, or the showcase).
  2. A very large site that takes more than a few seconds gets 202 with status: "publishing". Check GET .../status every 2 seconds until it reads live.

Good to know: Nothing runs on the server, so:

  • No logs. GET .../logs answers that a static site has no app logs.
  • No env vars. Setting or applying one is refused with 409 not_supported_for_static. Put configuration in the files.
  • No memory setting, no DATA_DIR, and no scale to zero.
  • One way only. A server app can be redeployed as a static site (its server is removed). A static site can't become a server app: that upload is refused with 422 app_kind_changed. Use a new app name.

Application / project identity

X-App-Name is the app's identity and also its public subdomain. There is no separate project id and no create-a-project step.

When you need it: Every call. Status, logs, env vars, history, rollback and showcase all use the app name.

How:

  1. Choose a name that matches ^[a-z0-9-]+$, 3 to 63 characters, with no leading or trailing dash.
  2. Slugify anything given in prose: "My Landing Page" becomes my-landing-page.
  3. Reuse it exactly for every later call on that app.

Good to know:

  • Names are unique across the platform, not per company. If another company holds the name, you get 409 app_name_taken. Choose a different name. This error does not mean "your app already exists". Redeploying your own app is a normal success.
  • Some names are reserved (ship, api, www, login, docs, admin, mail, and others). Treat a rejection as final.
  • The slug cannot change. A different slug is a different app on a different URL.
  • The display name can change with PATCH /api/apps/<name>. See Renaming an app. That never moves the URL.

Deployment status

The result of the latest deploy attempt, plus the app's current running state.

When you need it: After every deploy and every rollback, until it settles. Deploys run in the background, so this is how you know the app works.

How:

curl "https://ship.embarko.ai/apps/my-app/status"
  1. Poll every 10 seconds. status is building or publishing while it works.
  2. It ends as live, failed or crashed.
  3. Do what the reply's next field says:
    • while building, poll again
    • once live, give the user the URL, and claim the app if it is temporary
    • once claimed, offer the showcase
    • on a failure, read the logs it links to
  4. Report the live URL only on live.

A static site's deploy reply is usually already live, so there is nothing to poll.

Good to know:

  • No token while the app is an unclaimed anonymous deploy. Once claimed, the owning company's token is needed. A wrong token then returns 404, which never confirms the app exists.
  • Status is held in memory and shows the latest attempt only. For a lasting record, use deployment history.
  • Give up after about 10 minutes and read the logs.

Build errors

The output of a build that failed before the app started.

When you need it: status is failed and the app never came up. A failed build starts no container, so there are no runtime logs.

How: Use the same logs call. When there are no runtime logs, it returns the build output instead, with source: "build" and a note:

curl "https://ship.embarko.ai/apps/my-app/logs?stream=stderr"

It names the failing command. Read it before you change anything.

Good to know:

  • exit code: 127 is the most common sign. It means "command not found" in the build image: the build tool isn't in the app's dependencies, or the real app is in a subfolder whose dependencies were never installed.
  • A trailing docker-import ... no such file line is a side effect, not the cause.
  • Some build failures are refused before the build runs. They show as 422 codes on the deploy call. See troubleshoot.md.

Runtime errors and logs

The tail of the running app's stdout and stderr. It is the only view into a live app.

When you need it: The app is up but wrong, crashing, or restarting.

How:

curl "https://ship.embarko.ai/apps/my-app/logs"
curl "https://ship.embarko.ai/apps/my-app/logs?stream=stderr"
  • It returns { available, allocId, stream, logs }.
  • If nothing has ever deployed: {"available": false, "reason": "..."} with a next step.
  • A static site has no logs, and the reply says so.

Good to know:

  • A snapshot at call time, not a live stream. Poll to follow along.
  • About the last 8KB of output, with no retention promise. Always the most recent allocation, healthy or not.
  • OOM Killed / Exit Code: 137 means the app hit the 256MB limit.
  • Not Restarting / Exceeded allowed attempts is a crash loop. The real cause is logged above it.
  • For long-term logs, send them from inside your app to your own service.

Database

The app embeds its own database, SQLite via better-sqlite3, and writes the file under DATA_DIR, which survives every redeploy.

When you need it: The app needs structured data to outlast a deploy or restart: records, accounts, orders, content.

How: Open the database at a path under DATA_DIR, and create the folder on startup:

const dir = process.env.DATA_DIR;
fs.mkdirSync(dir, { recursive: true });
const db = new Database(path.join(dir, "app.db"));

Good to know:

  • There is no managed database service. Nothing to set up, no Postgres or MySQL to connect to, and no connection string to ask the user for.
  • One process means one writer. SQLite fits. A client/server database does not.
  • DATA_DIR survives redeploys, restarts and the app being stopped. It lives on one machine and does not survive losing that machine.
  • Not backed up. See Backups and restore.
  • Never call window.storage. It is a Claude Artifacts sandbox API and is refused at deploy time with 422 unsupported_storage_pattern.

Persistent file storage

Files written under DATA_DIR persist across redeploys. That covers user uploads, generated files and caches.

NOT AVAILABLE YET: a managed object store, meaning durable file storage separate from the app's own disk, with its own URLs, redundancy and lifecycle. DATA_DIR is one folder on one machine. It persists, but it is not an object store and has no redundancy.

When you need it: Any time the app accepts uploads or writes files it must keep.

How, today:

  1. Write under path.join(process.env.DATA_DIR, "uploads").
  2. Create the folder with { recursive: true } on startup.
  3. Serve files back through your own route.

Good to know:

  • Not backed up.
  • Disk has no quota, so you must limit it. Cap upload sizes and clear caches yourself. A runaway app affects the shared host.
  • No CDN in front of it, no signed URLs, no lifecycle rules.
  • Large media is a poor fit.

Secrets and app setup

Per-app environment variables, encrypted at rest and visible only to that app at runtime. Keep secrets out of the source: the tarball becomes an image, and anything in it ships with the app.

When you need it: Any API key, credential, or environment-specific setting. Not for static sites: they run no process, so env var writes to them are refused.

How: Needs a deploy token.

  1. Write the value.
  2. Apply it.
curl -X PUT "https://ship.embarko.ai/api/apps/my-app/env-vars/STRIPE_KEY" \
  -H "Authorization: Bearer $DEPLOY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"value": "sk_live_..."}'

curl -X POST "https://ship.embarko.ai/api/apps/my-app/env-vars/apply" \
  -H "Authorization: Bearer $DEPLOY_TOKEN"

Other calls: GET /env-vars lists keys, PUT /env-vars replaces all, DELETE /env-vars/<key> removes one.

Good to know:

  • A write alone does not reach the running app. Call apply or redeploy, or the app keeps the old value.
  • Values are write-only. GET returns keys only. No one holding the token can read a secret back, including you.
  • Keys must match ^[A-Za-z_][A-Za-z0-9_]*$.
  • This is not a managed secrets store: no rotation, no versioning, no sharing between apps.

Scheduled jobs

NOT SUPPORTED. There is no cron, no timer, and no way to have Embarko call your app on a schedule. An in-process scheduler (node-cron, APScheduler, a setInterval loop) is not reliable either: the process restarts on every redeploy and may be stopped when idle, so the schedule may never fire.

When you'd want it: Anything recurring: a nightly export, an hourly sync, a weekly email, reports, cleanups, digests, polling. An app that assumes a schedule will never run that code.

How, today: Keep the app on Embarko and trigger it from outside.

  1. Add an HTTP route that does the work.
  2. Protect it with a secret from env vars.
  3. Have an external scheduler call that route.

Don't fake it with an in-process timer. The app can restart or move at any time, so a setInterval is not a schedule.

Good to know: Tell the user the schedule is external until this ships. Do not report a scheduled feature as working just because the route exists.


Background jobs and workers

NOT AVAILABLE YET. Planned. There is no worker process, no queue, and no supported way to run work that outlasts the request that started it. Embarko runs one long-lived process per app, listening on PORT. Background work shares the same 256MB and CPU share with request handling, and is lost if the container moves.

When you'd want it: Image or video processing, long imports, retries with backoff, anything that takes minutes.

How, today:

  1. If the work is short, keep it inside the request.
  2. If not, run that piece off-platform. Have the Embarko app call it and store the result. See when Embarko doesn't fit.

Good to know: No queue, no retries, no concurrency control, and no instance count to raise. A job that outlasts its request may not finish.


Public HTTP endpoints and webhook routes

Every path on the app's URL reaches the app. The app does its own routing, including routes that receive webhooks from Stripe, GitHub, or anything else. There is no route registration, gateway setup or per-endpoint config.

When you need it: Any public API, form handler, callback URL or webhook receiver.

How:

  1. Handle the path inside the app.
  2. Give the third party https://<app-name>.embarko.app/<your-path>.
  3. Verify webhook signatures yourself, with a secret from env vars. The platform does not check inbound requests for you.

Good to know:

  • HTTPS only. No custom ports, no raw TCP, no private networking between apps.
  • Response headers, including Content-Security-Policy, are set by the platform at the edge. They override what your app sets.
  • There are no background workers. If a webhook expects a fast reply, acknowledge at once and keep the work short.
  • WebSockets are not supported. That includes libraries built on them (Socket.IO, ws, Pusher-style realtime). An app that needs live push (chat, multiplayer, live dashboards) will deploy, but its realtime features won't work. Use plain HTTP requests, polling from the browser on an interval.
  • Unconfirmed: request timeout, maximum request and response body size, and whether server-sent events work. Don't design around a value for these until they are documented.

Default Embarko domain

Every app is served at https://<app-name>.embarko.app from its first deploy. The user gets a shareable link at once, with no domain to buy and no DNS to set up.

When you need it: Always. It is the URL you hand back.

How: Nothing to do. It comes from the app name, so choose the name with care: it is the subdomain.

Good to know:

  • The name rules and reserved names in Application identity apply.
  • The subdomain cannot change. A different name is a different app.
  • It keeps working after a custom domain is added.

Custom domains

The user's own domain in front of the app, next to the default URL.

When you need it: The user asks for it and owns the domain.

How: An agent can do every step except the DNS record.

  1. Register the domain. The reply gives the exact DNS record to create.
  2. Hand that record to the domain's owner.
  3. After they create it and it spreads, verify.
# 1. Register it. Returns the exact DNS record to create.
curl -X POST "https://ship.embarko.ai/api/apps/my-app/domains" \
  -H "Authorization: Bearer $DEPLOY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"domain": "app.yourdomain.com"}'

# 2. After the owner creates that record and it propagates:
curl -X POST "https://ship.embarko.ai/api/apps/my-app/domains/app.yourdomain.com/verify" \
  -H "Authorization: Bearer $DEPLOY_TOKEN"

Other calls: GET /api/apps/my-app/domains lists what's registered. DELETE /api/apps/my-app/domains/<domain> removes one.

Good to know:

  • You cannot create the DNS record. Only whoever controls the domain can, at their DNS provider. This one step goes back to the user, and nothing gets around it.
  • A subdomain gets a CNAME. An apex gets an A record, which skips any CDN or DDoS proxy in front of it. Prefer a subdomain.
  • On Cloudflare the record must be DNS-only (grey cloud). A proxied record never verifies.
  • Verify is safe to call again and again while DNS spreads. Routing turns on only when it confirms.
  • The certificate uses an HTTP-01 challenge and needs port 80 reachable and not redirected.
  • Removing a domain never affects the default *.embarko.app URL.
  • Embarko's own addresses can't be registered (400 domain_reserved).
  • A domain belongs to one app at a time (409 domain_taken).
  • Works for server apps and static sites.
  • The app must be claimed first. These calls need its deploy token.

Automatic HTTPS

TLS certificates are issued and renewed automatically, for *.embarko.app and for connected custom domains. Nothing to buy, install or renew.

When you need it: Always on. There is no HTTP-only mode.

How: Nothing to do.

Good to know:

  • A custom domain gets its certificate only after its DNS verifies.
  • Issuing it needs port 80 reachable for the challenge.
  • Data is encrypted in transit. At rest, the storage provider encrypts it.

Application authentication (end-user login)

NOT AVAILABLE YET. Actively in progress.

What it will be: Sign-up, login and sessions for the app's own end users, provided by Embarko, so the agent doesn't build auth for every app.

What it is not: Authentication, which is the deploy token you use to call the platform. Never mix the two up in a message to a user.

When you'd want it: Any app with accounts: a CRM, a dashboard, anything per user.

How, today: Build auth into the app. The app is a real server, so sessions, a users table in SQLite under DATA_DIR, and hashed passwords all work. Do not wait for the platform feature, and do not tell the user one exists.

Good to know: Nothing is provided today: no identity provider, no OAuth integration, no session handling, no user store.


Access protection (private apps)

NOT AVAILABLE YET. Planned.

What it will be: A gate in front of the whole app, so only invited people can open it. End-user login gives an app users. This makes an app private.

When you'd want it: An internal tool, a client demo, a staging version that shouldn't be public.

How, today: Every deployed app is public to anyone with the URL. If it must be private, the app has to enforce that itself, for example with a password gate on every route. A hard-to-guess app name is not privacy.

Good to know: No IP allowlist, no basic-auth toggle, no preview protection. If the user assumes the app is private, tell them plainly the URL is public.


Deployment history

The lasting record of every deploy: its id, its version, when it ran, and whether it worked. It shows what is running, what changed, and what you can go back to.

When you need it: Before a rollback, and when the user asks what shipped.

How:

curl "https://ship.embarko.ai/api/apps/my-app/deployments" \
  -H "Authorization: Bearer $DEPLOY_TOKEN"

Newest first. GET /deployments/<id> returns one entry. Use it to poll a rollback too.

Good to know:

  • Needs a deploy token.
  • It is a record, not an archive. The build images behind old entries are pruned, so an entry can exist that you can no longer roll back to.
  • Pass a meaningful X-App-Version, or entries are just timestamps. Never reuse a tag for different code.

Rollback

Put an earlier successful build back in service. It switches to an image that already exists, with no rebuild, so recovery takes seconds.

When you need it: A deploy broke something and the previous version was fine. Restore first, debug after.

How:

  1. To go back to the previous good build, POST /rollback with {}.
  2. To pick one, list the history first and send its deploymentId.
curl "https://ship.embarko.ai/api/apps/my-app/deployments" \
  -H "Authorization: Bearer $DEPLOY_TOKEN"

curl -X POST "https://ship.embarko.ai/api/apps/my-app/rollback" \
  -H "Authorization: Bearer $DEPLOY_TOKEN" -H "Content-Type: application/json" \
  -d '{"deploymentId": "<deploymentId>"}'   # or '{}' for the previous good one
  1. It answers 202. Poll the returned deployment until it leaves in_progress.

Good to know:

  • Needs a deploy token.
  • Only successful deployments with a recorded image are valid targets. Pick the newest good one before the bad deploy, not just the previous entry.
  • It runs the old image with current settings: today's env vars and domains, not the old ones.
  • Only the last 5 builds are kept. A pruned target returns 422 rollback_image_unavailable. Its retainCount says how far back you can go.
  • History is not rewritten. A rollback is added as a new entry.
  • It does not restore data. DATA_DIR is untouched.

Backups and restore

NOT AVAILABLE YET. Actively in progress.

What it will be: Automatic backups of an app's persistent data, with a way to restore.

What exists today: Nothing. Data under DATA_DIR survives redeploys and restarts, and that is the whole promise. There is no snapshot, no backup, and no restore. Data is safe from your deploys, not from hardware loss.

When you'd want it: Any app with data the user would hate to lose, which is most of them.

How, today: Have the app export its own data: a scheduled dump the user can download, an endpoint that streams the SQLite file, or a copy pushed to storage they control. Tell the user this is the situation. Don't let them assume backups exist.

Good to know:

  • Rollback is not a backup. It restores code, never data.
  • Deleting an app destroys its data for good.

Application health

The health object in the status reply. It shows the app's current running state, apart from how the last deploy went. An app can be healthy while the newest deploy failed, because the previous version is still serving.

When you need it: To check that a live app is actually up, or after a deploy that worked but behaves wrongly.

How:

curl "https://ship.embarko.ai/apps/my-app/status"    # read the `health` field

Good to know:

  • There is no separate health endpoint, no uptime monitoring, no alerting, and no notice when an app goes down. You find problems by looking.
  • The platform restarts a crashed app a few times, then stops, and logs Not Restarting / Exceeded allowed attempts.
  • If the user needs to know about downtime, they need outside monitoring pointed at their URL.
  • Unconfirmed: the exact fields inside health, and whether a readiness/liveness check can be set per app. Branch only on the status reply's status field (live, crashed, …), which is specified.

Application isolation and security

Each app runs in its own environment, with its own filesystem and its own volume, on managed infrastructure certified to ISO/IEC 27001.

When you need it: The user asks about security, where data lives, or compliance. It decides whether an app can hold other people's data.

How: Nothing to set up. For a compliance answer, point to the DPA and Privacy Policy. Don't summarise them.

Good to know:

  • The ISO/IEC 27001 certificate belongs to the infrastructure provider. It covers their facilities and security management. It is not a certificate for Embarko. "Embarko is ISO 27001 certified" is false.
  • It does not cover embarko.ai or the dashboard, which run elsewhere.
  • Do not tell a user which country their app or data is in. There is no region choice, the region is not guaranteed, and the platform makes no data-residency promise outside the DPA.
  • Container-to-container network isolation is unverified. Don't treat it as a security boundary. Authenticate and encrypt anything sensitive.
  • Filesystem and storage isolation are real.

Resource and runtime defaults

What every app gets without asking: 256MB of memory, a shared CPU share, one process that stays on (unless it scales to zero), and a runtime detected from the source by Railpack. There is nothing to size or pick, so never ask the user about these.

When you need it: Designing the app, and diagnosing an OOM.

How: Build to the defaults.

  1. Read process.env.PORT.
  2. Bind 0.0.0.0.
  3. Ship no Dockerfile.
  4. Use a production start command.
  5. Keep a normal project layout so detection works.

Good to know:

  • Memory is 256MB and strictly enforced. Going over is an OOM kill (exit code 137), not a slowdown.
  • 256MB is the ceiling, not a reservation. An app is guaranteed 32MB and bursts up to 256MB when the host has room. Steady use well under the ceiling is safer than running near it.
  • There is no self-serve way to raise it. No agent call, and the dashboard control has been removed.
  • CPU is a relative share, not a reservation. A busy neighbour can slow you down, so add timeouts around CPU-heavy work.
  • No GPUs.
  • No Dockerfile or custom build step. Railpack supports some runtime serving overrides (a Caddyfile for static sites), but not replacing the build.
  • Disk has no quota, so you must limit it yourself.

Scale to zero

An opt-in that lets a server app sleep after a stretch with no traffic and wake on the next request. Good for an app that is used now and then.

When you need it: Decide before you build. Use it only when a few seconds' delay on the first request after a quiet spell is fine: an internal tool or a rarely used form, not a live dashboard.

How: Send X-Scale-To-Zero: true on POST /apps to opt in, or false to turn it off. Without the header nothing changes: apps stay on.

Good to know:

  • It sleeps after 6 hours with no traffic. The first request after that wakes it and is held, not refused. A full response typically takes 4 to 9 seconds.
  • The app must cope with restarting. Anything in memory is lost. Keep state under DATA_DIR, which survives the stop.
  • Automated traffic doesn't wake it. Credential scanners and crawlers get a 404 while it sleeps. Only real requests wake it.
  • Not for static sites, which have no process to stop.
  • WebSockets don't work with or without it. See Public HTTP endpoints.

Topic: GET /capabilities/scale-to-zero.


Delete application

Remove the running app, its routing, its image and its record.

When you need it: The user clearly asks to remove an app for good.

How: The token alone is not enough. Resend the app's own name to confirm.

  1. Show the user exactly what you will delete and get their go-ahead.
  2. Send the delete call:
curl -X DELETE "https://ship.embarko.ai/api/apps/my-app" \
  -H "Authorization: Bearer $DEPLOY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"confirmAppName": "my-app"}'

If confirmAppName is missing or wrong, it returns 400 confirmation_required and deletes nothing.

Good to know:

  • It cannot be undone, and there is no backup. The app's DATA_DIR data is destroyed, and the name is freed for anyone to take.
  • Always show the user exactly what you are about to delete and get their go-ahead first. confirmAppName guards against a misread instruction, not against an agent that is sure and wrong. See When to involve the human.

Renaming an app

Change an app's display name, the name the dashboard shows. The slug, and so the live URL, never changes. An app slugged my-app in a hurry can still show as "Customer Portal".

When you need it: The user dislikes the shown name, or it was made up automatically.

How:

curl -X PATCH "https://ship.embarko.ai/api/apps/my-app" \
  -H "Authorization: Bearer $DEPLOY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "My App"}'

Good to know:

  • Display name only. The URL does not move. For a different address, use a new deploy under a new name (a different app) or a custom domain.
  • Keep using the original slug in every other call.

MCP connector

An Embarko MCP connector lets an agent deploy through its tools instead of making HTTP calls. It shows in the connector list as Embarko. It needs no curl, no packaging and no token: it uses the account from the user's login. Its requests come from the connector's servers, so it works where a sandboxed agent cannot reach ship.embarko.ai at all.

When you need it: Whenever it is available, because it is the shorter path. Also when a direct call is blocked before it leaves your environment. See If you can't reach the API at all.

How:

  1. The user connects it in their AI tool's connector settings. You cannot install it for them.
  2. Send them to Connectors, which has the link for each AI tool.
  3. Once connected, it offers:
ToolWhat it does
Embarko OnboardingReturns the deploy instructions. Read it first
Deploy AppDeploys, given an app name and a source_url
Submit FeedbackReports a problem with Embarko itself

Good to know:

  • source_url must be a direct link to a .tar.gz that anyone can fetch, with no login wall and no redirect. The connector does not upload a local folder the way POST /apps does, so the source must be hosted somewhere reachable first. If it isn't, use the HTTP API.
  • There is no status or logs tool. The connector can start a deploy but can't tell you if it finished or why it failed. A deploy is not done when the tool call returns. With network access, poll status over HTTP. Without it, say plainly that you can't confirm it yet. Don't report success.
  • The same runtime rules apply as for any deploy: PORT and 0.0.0.0, no Dockerfile, DATA_DIR for anything lasting, 256MB. A deploy with no account connected is deleted after 24 hours.

Authentication: three ways in

There are three ways for a person or an agent to reach an Embarko account. All three lead to the same account. This is not the same as end-user login for the app itself.

Way inWho uses itWhat it gives
ConnectorAn AI tool with an Embarko connector (Claude today)The tool acts as the signed-in person. No token handled at all
Token by emailAn agent with no browserA deploy token, emailed to the person, which they paste back
Sign-in on embarko.aiA person, with GoogleThe dashboard, and from it, deploy tokens they can create and hand over

One account per email. All three match the person by email: a token requested for you@example.com and a Google sign-in as you@example.com reach the same account and the same workspaces.

When you need it: Claiming an app, and every control-plane call: env vars, history, rollback, showcase. Not needed for a first deploy, which needs no credential at all.

How: Use the first option that works. Full steps: Choose how to authenticate.

  1. A connector, if your tool lists one named Embarko. Nothing else is needed. If yours doesn't but the person's tool has one, give them the link from Connectors.
  2. A token you already have: $DEPLOY_TOKEN, or ~/.embarko/credentials.
  3. A new token, if neither exists. Request one by email (below), or the person signs in at embarko.ai/app/tokens, creates one, and pastes it to you.
curl -X POST "https://ship.embarko.ai/api/public/deploy-tokens/request" \
  -H "Content-Type: application/json" -d '{"email": "them@example.com"}'

Save a new token to ~/.embarko/credentials (chmod 600). Send it as Authorization: Bearer <token>.

Good to know:

  • The emailed token never reaches you. The call always returns 202, whether or not the address exists or you hit a limit. Ask the person to paste the token.
  • Rate limited to 5/hour per email and 10/hour per IP. The reply is the same either way, so call it once and never loop.
  • A new address gets an account. A known one gets new tokens. For an email with no account, one is created, with a workspace named after the address. For an email that has one, a new token is made for each of its workspaces and all are emailed. Existing tokens keep working.
  • A token belongs to a workspace, not a person. It only deploys. It does not sign in to the dashboard. For that, the person signs in on embarko.ai.
  • Tokens don't expire. They are shown once and stored hashed. A lost one can only be rotated, not recovered. The old one keeps working for one hour.
  • A token that is sent but invalid is always a hard 401. It is never treated as anonymous.

Account and project limits

A company's plan sets how many apps it can run at once and what usage is included. A deploy can be refused for reasons that have nothing to do with the app's code.

When you need it: Before deploying many apps for one user, or when a deploy is refused for a reason the error table doesn't explain.

How: Plans and limits are at embarko.ai/pricing, and can change apart from this page. Anything about the plan (upgrading, paying, changing a limit) is the user's decision and needs their approval.

Good to know:

  • There is no agent endpoint for a company's plan or remaining quota. Analytics reports one app's traffic, not the account's limits.
  • Public endpoints without a token have their own limits:
    • token requests: 5/hour per email and 10/hour per IP
    • anonymous feedback: 10/email, 20/IP and 500/hour globally

Also available

Useful, but outside the core flow above.

Anonymous deploys and claiming

A deploy with no credential that makes a real live app, which can be claimed later. No sign-up needed for the first deploy.

  • When: Every first deploy, unless you already hold a token.
  • How: Leave out Authorization. To claim, redeploy the same name with a token.
  • Good to know: Deleted 24 hours after creation. There is no reminder email. The only warnings are app.expiresAt and the claim steps in the status reply's next field once the app is live, plus a countdown banner added to HTML pages. A JSON-only API has no page to add it to.

Showcase listing

Show a deployed app on the public Showcase gallery or on an event Showcase page (a hackathon or demo day). An app has one listing, reused on every page it appears on.

  • When: Only when the user wants it shown publicly.
  • How:
    1. Check the current listing at GET /api/public/showcase/apps/<name> (no token).
    2. Draft the details from the project.
    3. Confirm them with the user.
    4. Send PUT /api/apps/<name>/showcase. Leave out collectionSlug for the public gallery. Send an event page's slug otherwise.
  • tagline and creatorName are required the first time. name defaults to the app's name, and changing it renames the app in the dashboard too. Every field and limit: List on Showcase.
  • Good to know: Needs a deploy token, so the app must be claimed. The public gallery publishes at once. An event page waits for its organiser unless the page's code is sent. Calling again merges, it doesn't error. An unknown slug returns 404 SHOWCASE_COLLECTION_UNKNOWN.

Sending feedback as an agent

Report a platform limit or a misleading doc from inside a deploy.

  • When: Something blocked you. Reporting it helps more than working around it.
  • How: POST /apps/<name>/customer-query with an X-Agent-Name header and {type, message}. Without an app, POST /api/public/customer-query with {type, message, email}.
  • Good to know: X-Agent-Name is required. type is the kind of problem: platform_bug, missing_capability, docs_issue, unexpected_behavior or other. message is 1 to 5000 characters. Full detail: API.

Analytics

Request traffic (total and by status code) and CPU/memory use.

  • When: The user asks how the app is doing, or you are checking load.
  • How: GET /api/apps/<name>/analytics?range=7d with a deploy token. range is 24h, 7d, 30d, 90d or all (default 24h). The same data is on the dashboard's Analytics tab.
  • Good to know: A 503 means the analytics system is unreachable. That is not zero traffic, so don't report it as zero. No alerting and no metrics export.

Summary: what exists and who can do it

CapabilityStatusWho
Deploy / redeployAvailableAgent
Application identityAvailableAgent
Deployment statusAvailableAgent
Build errorsAvailableAgent
Runtime logsAvailable (server apps only)Agent
Database (embedded SQLite)AvailableAgent
Persistent files under DATA_DIRAvailableAgent
Secrets / env varsAvailable (server apps only)Agent
Static sites (no build, no server)AvailableAgent
Public HTTP and webhook routesAvailableAgent
Default domain + automatic HTTPSAvailableAgent
Deployment historyAvailableAgent
RollbackAvailableAgent
Application health (field only)AvailableAgent
IsolationAvailableNone
Resource defaults (256MB, 1 process)FixedNone
Scale to zero (opt-in)AvailableAgent
Agent / API authenticationAvailableAgent
MCP connector (deploy, feedback)AvailableAgent (user connects it)
Showcase listingAvailableAgent
Rename (display name)AvailableAgent
Custom domainsAvailableAgent (DNS record: user)
Delete applicationAvailableAgent (confirmation required)
AnalyticsAvailableAgent
Account / project limitsPlan-basedUser
Backups and restoreNot available, in progressNone
Application authentication (end users)Not available, in progressNone
Access protection (private apps)Not available, plannedNone
WebSockets / realtime connectionsNot supportedNone
Scheduled jobs / cronNot supportedNone
Background jobs / workersNot available, plannedNone
Managed object storageNot available, plannedNone
Managed database serviceNot available, plannedNone
Memory / CPU adjustmentNot availableNone
GPUsNot available, not plannedNone

GET /capabilities/not-supported is the final word. Anything it lists is truly absent, however old this table is.