# Upuai Cloud — Full Agent Context This file is for AI coding agents that need the entire Upuai Cloud surface in a single fetch. Last updated: 2026-07-02. ## Deploy this project — short version If a user wants you to deploy their repo to Upuai right now (requires `upuai` CLI v0.10.0+ for source-aware `init` and `deploy --wait`): 1. Install or update the CLI: `brew install saiph-ti/upuai-cli/upuai` (macOS/Linux) or `scoop install upuai` (Windows). Verify `upuai version` is >= 0.10.0; if older, run `upuai upgrade` (or `brew upgrade upuai`). 2. Auth: run `upuai whoami` first. If `UPUAI_TOKEN` is set (CI, servers), it describes the token — `tokenName`, `scopes`, `workspace`/`workspaceId`/`workspaceSlug`, and `tokenProjectId`/`tokenProjectName` when the token is restricted to one project — and a revoked or expired token makes it fail (requires CLI v0.22.0+; older versions only read `~/.upuai/credentials.json`). Without a token: if it returns the user, credentials are persisted in `~/.upuai/credentials.json` and the CLI auto-refreshes the JWT on 401; you can skip ahead. If it fails, ask the user to run `upuai login` once in their own terminal (browser OAuth or email OTP — interactive), same pattern as `railway login` / `vercel login` / `fly auth login`. For CI/automation, a human mints a scoped token (`upuai token create --scope deploy`) and sets it in `UPUAI_TOKEN` (opaque, long-lived, revocable) — never a user JWT. 3. Init + connect source in a single shot: ```bash upuai init --name --repo / --branch main --framework --yes ``` - For docker images instead of a git repo: `--image ` (mutually exclusive with `--repo`). - For monorepos: add `--root-dir apps/api` (or `--root-dir .` to build from the repo root — CLI v0.24.1+, older versions silently drop the `.`). A Dockerfile in apps/api that needs the lockfile or sibling packages keeps `--root-dir apps/api` and sets the build context to the repo root: `upuai config set --builder dockerfile --docker-context ../..` (v0.28.0+), or `dockerContext = "../.."` in `apps/api/upuai.toml`. - Pick `--framework` from: Next.js, Vite, React, Node.js, Go, Django, Flask, Python, Rails, Docker, Static. Ask the user to confirm if the repo could match more than one. - Without `--repo` / `--image`, `init` creates an `empty` service that **cannot be deployed** until you attach a source via `upuai add --type github` or the dashboard. 4. Deploy and block until terminal status: ```bash upuai deploy --wait --yes -o json ``` `--wait` polls every 3 s until the deployment reaches a terminal status. From v0.27.0 there is no client-side timeout (the platform ends every deployment on its own; `--wait-timeout ` caps the wait if you need one). Older versions stop waiting after 300 s — shorter than about half of real deployments — so on them pass `--wait-timeout 3600`. Exit code is non-zero on `failed` / `cancelled` / `build_failed`. The final JSON object contains the live `url`. No connected git repo? Use `upuai up` instead (requires CLI v0.11.0+) to deploy the **local source** directly: ```bash upuai up --wait -o json ``` `upuai up` packages the current working directory into a tarball (honors `.gitignore`/`.upuaiignore`; excludes `.git`, `node_modules`, `.env*`), uploads it to platform storage, and triggers a deploy from that source — no GitHub App / git connection required. It reads the local `upuai.toml` just like the git path (release-phase/migrations apply). Git is the recommended, canonical path (`upuai deploy`, or `git push` for auto-deploy); `upuai up` is an escape hatch for local code when no repo is connected. `upuai up` is a SEPARATE command from `upuai deploy` (deploy from a CONNECTED github/gitlab repo) — `up` is **no longer an alias** of `deploy`. Flags: `--wait` (block until terminal status, non-zero exit on failure), `--wait-timeout ` (default 300). Auto-deploy on push is decided PER ENVIRONMENT: a push deploys each service environment whose configured source branch matches the pushed branch (e.g. a `dev` environment tracking `dev`, `production` tracking `main`) — never environments tracking other branches. Each service+environment exposes an "Auto-deploy on push" toggle in its source settings to opt out. Tag pushes and branch deletions never trigger deploys. In a monorepo (several services on one repo, each with a Root Directory), a push redeploys a service when it changes a file inside that service's Root Directory, or a file outside every service's Root Directory (shared packages, lockfile, root config); a service whose Root Directory is the repo root (`.` or unset) redeploys on every push. 5. If `status === "failed"` (build) or `crashed` (runtime), dump `upuai logs -n 50` and read the troubleshoot section below. 6. If the user's account hasn't authorized the Upuai GitHub App yet, `--repo` errors out with an auth message — direct them to https://app.upuai.com.br/projects/new once to install it, then retry. For richer guidance (route-first skill): on Claude Code the `upuai` CLI (v0.17.0+) installs it for you — it writes `.claude/skills/upuai/SKILL.md` on the first command in a linked project (existing projects included), or run `upuai skill install`. For other agents, install once with `npx skills add saiph-ti/upuai-cli --skill upuai`. Same content as below but structured for agent consumption. Public guide: https://upuai.com.br/docs/ai-deploy. ## What Upuai Cloud is A Brazilian PaaS. Push source → it builds with railpack → deploys to a managed Kubernetes runtime with one-click horizontal and vertical scaling (no autoscaling), secrets, volumes, custom domains, MinIO buckets. Multi-tenant. Plans: FREE, STARTER, PLUS, PRO, BUSINESS, ENTERPRISE. There is NO permanent free plan (since 2026-09-09): FREE is the 15-day TRIAL tier every workspace starts on, no credit card; the 15 days count from the workspace's first service going live (first successful deploy, or first database/bucket provisioned) and nothing expires before that (the start is recorded once — deleting everything later does not reset it); when the trial ends without a paid plan the workspace's services are paused (`TRIAL_EXPIRED` on any create/deploy/scale/restart), data is kept for 15 more days, then projects are deleted — subscribing at any point reactivates immediately. Each plan grants a RUNTIME compute envelope to the WHOLE workspace — shared by every environment, never per environment: FREE 2 vCPU/2 GiB/4 GiB storage · STARTER 4/4/10 · PLUS 8/8/25 · PRO 16/16/50 · BUSINESS 24/24/100 · ENTERPRISE 64/64/500 (ENTERPRISE and negotiated contracts can carry a custom envelope; a workspace subscribed before a plan's limits changed keeps the limits it subscribed to while it stays on that plan — e.g. STARTER 6/6/10 or BUSINESS 32/32/100 contracted before 2026-10 — and moves to the current limits when it changes plan). The envelope is the ONLY compute rule of a plan — there is no per-service cap: one replica can use the whole envelope (capped at 32 vCPU / 64 GiB per replica), so a STARTER can run one 4 vCPU replica or four of 1 vCPU. What consumes the envelope is what the services DECLARE in steady state — CPU/memory limit × replicas of every process and the main container of every managed database — plus the provisioned size of volumes/databases, summed across ALL environments; never measured usage, platform sidecars (backup, metrics) or the transient pods of a deploy, so a formation that fits also redeploys at the full envelope. A scale, deploy, resize or new database that would exceed the workspace CPU/memory envelope fails with `PLAN_ENVELOPE_EXCEEDED`; a new volume or database above the storage envelope fails with `PLAN_LIMIT_STORAGE`; a namespace out of pod slots fails with `POD_QUOTA_EXCEEDED`. Every process runs at least 1 replica (`instanceCount`/`instances` minimum 1; delete a service that should not run). The FREE trial runs exactly 1 replica per process: a scale or an upuai.toml `instances` above the plan's per-process maximum fails with `PLAN_LIMIT_INSTANCES`. A service the platform stopped shows as STOPPED: after a sustained crash loop, `upuai restart` (or Restart in the dashboard) scales it back; when the workspace was suspended (trial ended, unpaid), it comes back on its own when the workspace is reactivated. The dashboard (Settings → Resources, Observability) shows "allocated / envelope" for the workspace with each environment's share, in vCPU (1 vCPU = 1000m). Builds run on a dedicated per-plan runner: FREE 4 vCPU/4 GB · STARTER, PLUS and PRO 6 vCPU/6 GB · BUSINESS and ENTERPRISE 8 vCPU/10 GB (BUSINESS/ENTERPRISE tier has 2 parallel runners; one build at a time per runner). Excess builds queue and are never failed for waiting — a newer push to the same service supersedes the queued one, and the queue only fails fast when the build infrastructure itself is down (`BUILD_BUCKET_DOWN`). The MEMORY limit is strictly enforced — exceeding it fails the build with `BUILD_OOM` — so size heaps and parallelism to these plan numbers (e.g. Next.js `experimental.cpus`, `make -j`, Gradle `workers.max`), not to whatever `MemTotal` reports. Datacenters in BR. ## Rules every agent must follow 1. **Default to railpack.** Do NOT generate a Dockerfile to make a deploy work. Railpack auto-detects Node, Python, Go, Rails, Bun, Deno, Elixir, Java, and more. A `Dockerfile` in the repo does NOT switch the builder — it is opt-in only. The user must explicitly choose `dockerfile`. 2. **Migrations belong in `upuai.toml`**, not in a custom `Dockerfile`, an `entrypoint.sh`, a CI step, or a manual `prisma migrate deploy`. Set `[deploy] releaseCommand`. The platform runs it in an ephemeral Job before each rollout with the same image and env vars; failure blocks traffic shift, old pods keep serving (zero downtime). 3. **CLI minimum version: 0.4.0.** Bucket services, `upuai logs --build/--deploy`, and `upuai config show` require this version. If you hit "invalid service type 'bucket'" or "unknown flag", run `upuai upgrade` first. Verify with `upuai version`. 4. **Auth.** `upuai login` (browser OAuth or email OTP) is the canonical flow for humans — same pattern as `railway login`, `vercel login`, `fly auth login`. Credentials persist in `~/.upuai/credentials.json` and the JWT auto-refreshes on 401. For **CI/automation**, mint a scoped, revocable token with `upuai token create --scope deploy` and set it in `UPUAI_TOKEN` (opaque, long-lived, no refresh; a 401 means it was revoked/expired). Combine `--yes` (or `-y`) with explicit flags to skip wizards. Example: `upuai init --name my-app --repo myorg/repo --framework "Node.js" --yes`. Suppress the update-available nudge with `UPUAI_DISABLE_UPDATE_CHECK=1`. 5. **Don't pre-create local Postgres/Redis containers.** The platform provisions databases as services; the runtime injects `DATABASE_URL`, `REDIS_URL` etc. as env vars. Add a database with `upuai add --type database --name postgres`. 6. **Don't filter/sort/aggregate API data client-side.** All Upuai API endpoints accept query params. The CLI already does this correctly. ## Install ```bash # macOS / Linux brew tap saiph-ti/upuai-cli brew install upuai # Windows (Scoop) scoop bucket add upuai https://github.com/saiph-ti/scoop-upuai-cli scoop install upuai # Linux / macOS without Homebrew (servers, CI runners) — verifies the SHA-256 curl -fsSL https://raw.githubusercontent.com/saiph-ti/upuai-cli/main/install.sh | sh # Verify upuai version # must be >= 0.10.0 for the source-aware `init` and `deploy --wait` flows below ``` ## Auth People sign in interactively (same pattern as `railway login`, `vercel login`, `fly auth login`): ```bash upuai login # browser → GitHub OAuth upuai login --email # alternative: email OTP (6-digit code) upuai whoami # verify ``` Credentials persist in `~/.upuai/credentials.json` (file perms `0600`); the JWT auto-refreshes on 401. CI/CD and servers use a scoped machine token instead of a login. A workspace owner or admin creates it once and the pipeline reads it from `UPUAI_TOKEN`: ```bash upuai token create --name ci --scope deploy --project # prints the secret once export UPUAI_TOKEN=upua_... # CI secret upuai deploy -p -s -e production --wait --yes -o json ``` `--scope read` allows only GET requests, minus the two reads that hand out write access — a container shell (`upuai ssh`) and bucket credentials both need `--scope deploy`. A token operates project resources (deploys, services, variables, domains, databases); members, billing and workspace settings always require a signed-in person. A 401 means the token was revoked or expired. `upuai whoami` with `UPUAI_TOKEN` shows the token's name, scopes, workspace and project. Tokens can also be managed in the dashboard (Settings → API tokens). ### Update an image service from CI Every deploy of an image service pulls the image again (a redeploy picks up whatever a mutable tag points to). To move a pinned tag, `upuai deploy -s --image --wait --yes` sets the image and deploys; it refuses git services. Needs CLI v0.22.0+ ("unknown flag --image" means the runner has an older build). Read the current image with `upuai config show -s -o json | jq -r '.config.source.image'`. GitHub Actions example that deploys only when Docker Hub has a newer release: ```yaml name: Update mailpit on: schedule: - cron: '0 6 * * *' workflow_dispatch: jobs: update: runs-on: ubuntu-latest env: UPUAI_TOKEN: ${{ secrets.UPUAI_TOKEN }} # upuai token create --scope deploy --project UPUAI_DISABLE_UPDATE_CHECK: '1' IMAGE: axllent/mailpit steps: - name: Install the Upuai CLI run: curl -fsSL https://raw.githubusercontent.com/saiph-ti/upuai-cli/main/install.sh | sh - name: Latest release tag on Docker Hub id: hub shell: bash # -eo pipefail: a failed curl or an empty match stops the job run: | tag=$(curl -fsSL "https://hub.docker.com/v2/namespaces/axllent/repositories/mailpit/tags?page_size=100&ordering=last_updated" \ | jq -r '.results[].name' | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | sort -V | tail -1) echo "image=$IMAGE:$tag" >> "$GITHUB_OUTPUT" - name: Image running on Upuai id: current shell: bash run: | current=$(upuai config show -p my-project -s mailpit -e production -o json | jq -er '.config.source.image') echo "image=$current" >> "$GITHUB_OUTPUT" - name: Deploy the new tag if: steps.hub.outputs.image != steps.current.outputs.image run: upuai deploy -p my-project -s mailpit -e production --image "${{ steps.hub.outputs.image }}" --wait --yes ``` ## First deploy from scratch (interactive) ```bash # 1) initialize the project (no prompts) upuai init --name my-app --framework "Node.js" -e production -y # 2) (optional) commit an upuai.toml so build/deploy lives in the repo cat > upuai.toml <<'EOF' #:schema https://upuai.com.br/schemas/upuai-toml-v1.json [deploy] releaseCommand = "pnpm exec prisma migrate deploy" healthCheckPath = "/health" EOF # 3) deploy upuai deploy # 4) inspect upuai status upuai logs # last 100 runtime lines upuai logs -f # live tail (SSE) upuai logs --build # build log of latest deployment upuai logs --deploy # release-phase + rollout log of latest deployment ``` ## upuai.toml (schema v1) Path: at the root of the service's `rootDirectory`. All keys optional. Schema URL: `https://upuai.com.br/schemas/upuai-toml-v1.json`. ```toml #:schema https://upuai.com.br/schemas/upuai-toml-v1.json [build] builder = "railpack" # default; alternative: "dockerfile" buildCommand = "pnpm build" # optional override of railpack dockerfilePath = "Dockerfile" # only when builder = "dockerfile"; relative to the root directory dockerContext = "../.." # optional Dockerfile build context, relative to the root directory (monorepo: repo root) [deploy] startCommand = "node dist/server.js" releaseCommand = "pnpm exec prisma migrate deploy" releaseTimeoutSeconds = 300 # default 300, range 10..1800 healthCheckPath = "/health" ``` Precedence: UI service settings (DB) > `upuai.toml` > Procfile `release:` line (legacy compat). Empty string in the UI does NOT override `upuai.toml` — only a non-empty value. ### `[processes.*]` — Procfile App Declare multiple processes from one repo/build (Heroku Procfile style). **One push builds the image once and runs N processes sharing it**, promoted atomically. Declaring ≥2 processes (web/worker/clock) is the opt-in — there is no feature flag. ```toml [processes.web] command = "bundle exec puma" [processes.worker] command = "bundle exec sidekiq" type = "worker" # optional; inferred from name: web→web, worker→worker, clock/cron→clock, release→release, else→worker instances = 2 # optional; default 1 [processes.release] command = "bundle exec rails db:migrate" type = "release" # not a runtime — runs in the release phase like deploy.releaseCommand ``` One push builds the image once; all processes share that image + the service's env vars (Heroku config-vars parity). Only one `web` process is allowed. Manage per-process with `upuai ps`, `upuai scale web=2 worker=1`, `upuai logs --process worker`, `upuai restart --process worker`, or the dashboard Processes tab. ## releaseCommand by framework | Stack | releaseCommand | | ------------------------------ | ------------------------------------------------------------------ | | Prisma (pnpm monorepo) | `pnpm --filter exec prisma migrate deploy` | | Prisma (single package) | `npx prisma migrate deploy` | | Drizzle | `pnpm exec drizzle-kit migrate` | | TypeORM | `pnpm typeorm migration:run` | | Knex | `pnpm knex migrate:latest` | | Sequelize | `pnpm sequelize db:migrate` | | Django | `python manage.py migrate --noinput` | | Alembic (SQLAlchemy) | `alembic upgrade head` | | Rails | `bundle exec rails db:migrate` | | Laravel | `php artisan migrate --force` | | Phoenix / Ecto | `mix ecto.migrate` | | golang-migrate | `migrate -path migrations -database $DATABASE_URL up` | | atlas | `atlas migrate apply --url $DATABASE_URL` | ## Builders **railpack (default — always)**: zero config. Detects Node (Next/Nest/Nuxt/Remix/SvelteKit/Express/Fastify/Hono/Astro/etc.), Python (FastAPI/Django/Flask), Go (Gin/Echo/Fiber/Chi), Ruby (Rails/Sinatra), Java (Spring Boot/Quarkus/Micronaut), Bun, Deno, Elixir/Phoenix, static SPAs (Vite/Astro static — auto-injects Caddyfile). Node version: declare `engines.node` in the app's `package.json` (railpack also reads `.node-version`/`.nvmrc` in the app directory). The same version builds server apps and static sites; without a declaration the platform default applies (Node 22); a railpack upgrade does not change it. To pin a version without touching the repo, set the build variable `RAILPACK_NODE_VERSION` (it overrides `engines.node`). **dockerfile (opt-in only)**: never auto-detected. Choose explicitly: ```bash # At service creation (Dockerfile at the root of the root directory) upuai add --type github --name api \ --repo https://github.com/org/repo \ --builder dockerfile --dockerfile-path Dockerfile # Monorepo: apps/api/Dockerfile needs the lockfile and sibling packages (v0.28.0+) upuai add --type github --name api \ --repo https://github.com/org/monorepo \ --root-dir apps/api --builder dockerfile --docker-context ../.. # Or after the fact upuai config set --builder dockerfile --dockerfile-path Dockerfile # Switch back to railpack upuai config set --builder railpack ``` ## CLI commands (v0.4.0+) ``` upuai login | logout | whoami upuai token create [--name N --scope read|deploy --project ID --expires DAYS] | token list | token revoke ID upuai init [--name N --framework F -e -y] upuai link | unlink | list (ls) | open | delete | status upuai workspace list | workspace current | workspace switch [slug|name|id] # aliases: ws, workspaces upuai deploy [--watch] # deploys the connected repo upuai deploy --image # docker-image service: sets the tag and deploys it (v0.22.0+) upuai up # deploys the local working tree — SEPARATE command, not an alias upuai redeploy [--rebuild] | rollback | promote | down # redeploy reuses the image when build inputs are unchanged; --rebuild builds again (v0.28.0+). Every redeploy starts a NEW instance and swaps it in once healthy — it replaces an instance that stopped responding upuai add [--type app|bucket|database|docker|docker_image|function|github|gitlab --name N --image I --repo URL --branch B --root-dir D --builder railpack|dockerfile --dockerfile-path P --docker-context DIR --start-command CMD --health-check PATH --health-check-timeout SEC] upuai restart | scale N | ps | run -- | shell # restart also recreates instances that stopped responding (not Ready); a service with no replicas is not restarted — scale it up upuai ssh [-s SERVICE] [-n] [-- COMMAND...] # session INSIDE the running container; -n/--no-stdin never attaches stdin (needs a command, no PTY) — use it from CI and agents (v0.25.0+) upuai service delete # delete ONE service (deployments/volumes/buckets/domains); vs `delete` = whole project upuai logs [-n N | -f | --build | --deploy | -d ] upuai db connect | db backup | db restore upuai db public [enable [--allow ]... [--any] | disable] # public endpoint + IP allowlist (v0.23.0+) upuai db extensions [enable|disable|update ] | db version | db update [--wait [--wait-timeout SEC]] # managed Postgres extensions + maintenance update (v0.26.0+) upuai volume list | volume add --path --size | volume remove # persistent disk (v0.24.0+) upuai env list | env switch | env new | env delete upuai config show | config set [--builder X --dockerfile-path P --docker-context DIR --build-command CMD --start-command CMD --health-check PATH --root-dir D --health-check-timeout SEC] upuai vars list | vars set KEY=VAL [-s service] [--secret] | vars delete KEY # --secret: masked in listings, never returned by the API; an existing secret stays secret on update (v0.25.0+) upuai domain list | domain add HOST [--redirect-to HOST --status 301|302] upuai domain update HOST --redirect-to HOST | --no-redirect | --status 301|302 upuai domain generate | domain delete HOST # alias: `domains` upuai template list | deploy upuai catalog list | describe | deploy upuai stack list | get | delete upuai bucket public show | bucket public enable | bucket public disable upuai scheduler list | create | run | pause | resume | delete # aliases: cron upuai scheduler create --name N --command CMD --schedule "" | --once # --once: run the command a single time, now; the job never runs on its own; --timeout SEC kills a run (default 300, max 1800) (v0.25.0+) upuai version | completion upuai skill install | skill status upuai upgrade [--check] # detects brew/scoop and delegates; --check previews upuai admin storage pvcs # ADMIN role only ``` The CLI prints a one-line stderr nudge when a newer release is available (cached 24h, opt out via `UPUAI_DISABLE_UPDATE_CHECK=1`). Global flags: `--project ID|NAME`, `-e/--environment NAME`, `--output table|json|text`, `-y/--yes`, `--verbose`. There is deliberately **no** `--workspace` flag: switching rotates the whole session's tokens, so a per-command override would leave you in another workspace after the command ends. The workspace comes from the directory pin instead (see below). ### Workspaces (CLI v0.19.0+) A session is scoped to ONE workspace at a time (`workspace → project → environment → service`). Anything outside it answers 404, indistinguishable from "does not exist" — an empty `upuai ls` usually means "wrong workspace". `upuai init`/`upuai link` record `workspaceId` in `.upuai/config.json`, and any command resolving the linked project or service realigns the session first, announcing `→ workspace: ` on **stderr** (stdout stays clean for `-o json`). Directories linked before the field existed are healed automatically on the next command. The realignment follows the **target** of the command, not the directory: with `-p` naming another project, the directory's pin no longer moves the session, and its `environmentId`/`serviceId` are not reused for it. `upuai link ` of another workspace switches to it and links (adopting the workspace *is* what linking means). `-p ` of another workspace does not switch — it reports which workspace owns the project, and running the `upuai workspace switch` it suggests then makes the same command work. A `-p` for another project on a command that needs a service, without `-s`, is refused rather than applied to this directory's service. Machine tokens (`UPUAI_TOKEN`) are bound to the workspace they were minted in and cannot switch. ## Service types - `app` — generic empty container, populate via `--image` or `--repo` later - `bucket` — MinIO S3-compatible (env vars injected: `S3_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_ENDPOINT`) - `database` — managed Postgres / MySQL / MongoDB / Redis (interactive picker, env vars auto-injected) - `docker` / `docker_image` — bring-your-own image - `function` — serverless with cold-start optimization - `github` / `gitlab` — Git source, auto-build on push (railpack default) ### Persistent disk on a service `upuai volume add --path /data --size 5` creates a disk and mounts it on the service; the files survive deploys and restarts. The disk is ReadWriteOnce, so two things become true and stay true: the service runs a SINGLE replica (scaling it is refused), and every deploy stops the old pod before starting the new one — a few seconds of downtime, the same trade Railway and Fly make for stateful services. Mount paths must be absolute and outside the container's system directories (`/data`, `/var/lib/postgresql/data` and `/usr/share/nginx/html` are fine; `/etc`, `/usr/lib` and friends are refused). Disks are provisioned on a storage class with a daily block backup kept for 7 days. `upuai volume list` shows size and mount point, `upuai volume remove ` detaches and deletes it (files are lost). Same controls in the dashboard under the service's Volume tab. Needs CLI v0.24.0+. ### Reaching a managed database from outside A managed Postgres is private by default. `upuai db public enable` publishes it at `.db.upuai.cloud:5432` (TLS, `sslmode=verify-full`). Restrict who may connect with `--allow`: `upuai db public enable --allow 203.0.113.7 --allow 10.0.0.0/8` accepts only those origins and refuses everything else at the edge, before Postgres — the usual case is a CI runner or a VPS with a fixed IP. `--any` opens it to the internet (password and TLS are then the only barrier), `upuai db public disable` unpublishes it, and `upuai db public` prints the current state. Running `enable` with no flag keeps the current allowlist. Same controls in the dashboard under the database's Connect tab; changing them needs owner or admin. ### Postgres extensions (PostGIS, pgvector, …) The platform's current managed Postgres image ships PostGIS, pgvector and a curated set of extensions (pg_trgm, unaccent, citext, hstore, ltree, uuid-ossp, pgcrypto, pgrouting, …). They are enabled per database, in the `app` database, with no restart: `upuai db extensions` lists them (available / enabled / versions), `upuai db extensions enable postgis` runs `CREATE EXTENSION … CASCADE` (dependencies come along), `upuai db extensions disable ` runs `DROP EXTENSION … RESTRICT` — refused (`DB_EXTENSION_IN_USE`, with the dependent objects listed) while columns or other extensions depend on it; nothing is ever dropped in cascade — and `upuai db extensions update ` moves an enabled extension to the image's version. The `postgres` user can also run `CREATE EXTENSION` through SQL (application migrations). Extensions that need `shared_preload_libraries` (pg_stat_statements, pgaudit, pg_cron) are not offered. Same controls in the dashboard: database → Extensions tab. Needs CLI v0.26.0+. A database created before this image line reports `DB_EXTENSION_UNAVAILABLE` for PostGIS: apply its maintenance update first. `upuai db version` shows whether one is pending and `upuai db update [--wait]` applies it — same PostgreSQL version, the platform's current image, the database restarts for about 1–2 minutes (dashboard: Settings → Version → Apply update). A major version change is refused with `DB_MAINTENANCE_REQUIRED` while the update is pending. Applying the update is self-service (dashboard or CLI); nothing is applied to a database without its owner asking. If it is refused with `DB_MAINTENANCE_COLLATION_RISK` (the database uses ICU / non-C collations whose ordering changes with the new OS), tell the user to contact support — it is scheduled with a reindex, never forced. Recipe for agents ("the app needs PostGIS" / "add pgvector"): ```bash upuai db extensions -s -o json # find the extension: available? installed? # available + not installed → enable it (instant, no restart): upuai db extensions enable postgis -s # unavailable (older image) → tell the user it restarts the DB ~1–2 min, then: upuai db update -s --yes --wait && upuai db extensions enable postgis -s ``` Alternatively keep it in the app's migrations — `CREATE EXTENSION IF NOT EXISTS postgis;` works because the `DATABASE_URL` user owns the `app` database (Django `CreateExtension`, Rails `enable_extension`, a raw SQL migration…). Never build a custom Postgres image or run Postgres in an app container to get an extension. Full reference: https://upuai.com.br/docs/postgres ### Managed MySQL: account, credentials, rotation A managed MySQL (8.4 or 8.0) is reachable from services of the same environment by its service name (`:3306`). Public access is opt-in (panel → database → Connect tab, OWNER/ADMIN): the database gets its own address `.db.upuai.cloud` on a port dedicated to it (MySQL has no SNI, so every database has its own port; it is stable across disable/enable), with a Let's Encrypt certificate served by mysqld — connect with `--ssl-mode=VERIFY_IDENTITY` (DBeaver: Use SSL + Verify server certificate; JDBC: `sslMode=VERIFY_IDENTITY`). A database created before public access existed restarts once (~1 min) on the first enable. The platform creates the application account and keeps the credential in use. The variables on the database service — `HOST`, `PORT`, `MYSQL_PORT`, `MYSQL_USER` (`app`), `MYSQL_PASSWORD`, `MYSQL_DATABASE` (`app`), `DATABASE_URL` — mirror it and are **read-only**: editing or deleting one is refused (`409 OPERATION_NOT_ALLOWED`, reason `databaseCredentialsReadOnly`), and it would not change the database anyway. There is no root account. The database service cannot be renamed (its name is its hostname). - Wire a service by reference: `upuai variables set 'DATABASE_URL=${{.DATABASE_URL}}' -s `, then deploy. References resolve at deploy time, so a password change reaches the service on its next deploy. - The account can create databases, tables, views, triggers, routines, events and other accounts. For a database other than `app`, create it over SQL and build the URL: `mysql://${{.MYSQL_USER}}:${{.MYSQL_PASSWORD}}@${{.HOST}}:3306/`. Not granted: server administration (global variables, shutdown, files) and writes to the `mysql` system schema. Accounts prefixed `moco-` belong to the platform — never alter them. - Authentication is `caching_sha2_password` (MySQL 8 default): use a current driver (`mysql2` on Node, not the legacy `mysql` package). - `upuai db credentials repair -s ` proves the login and re-applies the account if the server refuses it (password unchanged). `upuai db credentials rotate -s --yes` issues a new password (owner/admin); the old one stops opening connections at once and the command lists the services to redeploy. Same actions in the dashboard: database → Database tab → Credentials. Needs CLI v0.27.0+. - `upuai ssh` into a managed MySQL is refused by design; run SQL from one of your own services with a MySQL client and `DATABASE_URL`. Recipe for agents ("Access denied for user 'app'" / "reset the MySQL password"): ```bash upuai variables list -s -o json # is DATABASE_URL the reference ${{.DATABASE_URL}}? upuai db credentials repair -s # re-applies the account if the database refuses it upuai deploy -s --wait --yes # only for the services the command lists ``` Never delete and recreate the database, edit its variables, or replace it with a `mysql` Docker image to "fix" an access error — the last one loses the managed backup. Full reference: https://upuai.com.br/docs/mysql ## Custom domains ```bash upuai domain add app.example.com # prints CNAME / TXT records to set upuai domain list # shows the redirect column ``` DNS auto-validation runs every 60s. TLS via Let's Encrypt (auto-renew). Domain Connect supported. Adding an apex domain (`example.com`) also creates `www.example.com` — and vice versa. The pair counts as ONE domain against the plan quota, and the counterpart is created redirecting (301) to the host you added, so search engines see a single canonical URL. Pairs created before 2026-08-26 still serve both hosts; turn the redirect on with one command. ```bash upuai domain add www.example.com --redirect-to example.com --status 301 upuai domain update www.example.com --redirect-to example.com # pick the canonical host upuai domain update www.example.com --no-redirect # both hosts serve again ``` Any domain of a service can redirect to another one of the SAME service (a generated `*.apps.upuai.cloud` host can redirect to the custom domain too). Path and query string are preserved; 301 (default) or 302; no redeploy — it applies within ~2 minutes. Chains are rejected: the target cannot itself redirect. Wildcard domains cannot redirect. Checking it: `curl -I` sends HEAD, and Traefik answers **308** to a permanent redirect on non-GET methods. Search engines use GET and get **301**. Verify with `curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://www.example.com/`. HSTS is opt-in per service, via `upuai.toml` (the platform never enables it for you — it is sticky in the visitor's browser and would break any HTTP-only subdomain of yours): ```toml [http.headers] Strict-Transport-Security = "max-age=31536000" ``` Redirects in `[[http.redirects]]` match a PATH and apply to every hostname of the service; redirecting between DOMAINS is an attribute of the domain (the commands above). Full guide: https://upuai.com.br/docs/custom-domains ## Project transfer between tenants/workspaces Move a project from one tenant/workspace to another from the project Settings tab ("Transfer project" dialog) — API: `POST /projects/:id/transfer`. A pre-flight check validates the **destination** plan caps before the move and fails fast with a clear error if the project can't fit: `maxProjects`, `maxCustomDomains`, `maxServicesPerProject`, `maxEnvironmentsPerProject`. ## Troubleshooting | Symptom | Likely cause | Fix | | ------------------------------------------------ | ----------------------------------------------------------- | ----------------------------------------------------------------- | | `409 TRIAL_EXPIRED` on create/deploy/scale/restart | the workspace's 15-day free trial ended (or its paid plan lapsed) and services are paused; data kept 15 more days, then projects are deleted | not a code problem — the user must pick a plan (dashboard → Settings → Billing); reactivation is immediate. Do not retry | | `invalid service type 'bucket'` | CLI < 0.4.0 | `upuai upgrade` (or `brew upgrade upuai`) | | `release phase timed out` | migration > 300s | bump `releaseTimeoutSeconds` (max 1800) or split migrations | | Build using railpack when you wanted Dockerfile | builder is opt-in; Dockerfile present is not enough | `upuai config set --builder dockerfile --dockerfile-path ` | | `503 ORCHESTRATOR_CIRCUIT_OPEN` on logs/restart | downstream orchestrator transient error; circuit cooldown | retry after 10s; falls back to live stream when MinIO durable hit | | `upuai logs` empty | service not deployed yet, or no replicas running | `upuai status` to confirm; `upuai logs --build` for build issues | | `ER_ACCESS_DENIED_ERROR` / `Access denied for user 'app'` on a managed MySQL | the service holds a stale password (not redeployed after a rotation), or the account was changed inside the database | `upuai db credentials repair -s `, then deploy the services it lists — never recreate the database | | `Plugin 'mysql_native_password' is not loaded` (1524) connecting to a managed MySQL 8.4 | the user in the connection URL does not exist on the server (mistyped, or a URL from another database) — MySQL 8.4 answers this instead of `Access denied` for some unknown users | use the reference `${{.DATABASE_URL}}`; then `upuai db credentials repair -s ` | | Health check failing | `healthCheckPath` not answering 200 within `healthCheckTimeout` (default 300s) | bump `healthCheckTimeout` or fix the endpoint | | Build fails with `BUILD_OOM` (compiler killed) | build exceeded the plan's build memory (FREE 4 GB, STARTER/PLUS/PRO 6 GB, BUSINESS/ENTERPRISE 10 GB) | limit parallelism (Next.js `experimental.cpus`, `make -j2`, Gradle `workers.max`) or upgrade the plan | | Deploy fails with `BUILD_BUCKET_DOWN` | the build infrastructure for your plan has no active runner (platform fault, auto-alerted) | not your code — retry in a few minutes once the runner recovers | | Deploy stuck on "Na fila — aguardando runner" | your plan's runner is busy with another build; queued deploys never expire for waiting (FIFO order, higher plans get queue priority) | wait (a newer push supersedes the queued one), or upgrade — BUSINESS/ENTERPRISE has 2 parallel runners | ## Resources - Dashboard: https://app.upuai.com.br - Status: https://status.upuai.com.br - CLI source: https://github.com/saiph-ti/upuai-cli (MIT) - Schema: https://upuai.com.br/schemas/upuai-toml-v1.json - Docs: https://upuai.com.br/docs · CLI: https://upuai.com.br/docs/upuai-cli · TOML: https://upuai.com.br/docs/upuai-toml