fundaia
The command line for the Fundaia deployment platform: projects, services, variables, deployments, logs and metrics, from a terminal.
It talks to any instance of the platform, not just one — --url and profiles are first-class,
so the same binary reaches production, staging and a laptop.
Install
Or take a prebuilt binary from the releases:
|
The script downloads the binary for your platform, verifies its checksum and puts it in
~/.local/bin. It never uses sudo; if that directory is not on your PATH it says so rather
than editing your shell profile behind your back.
Sign in
A terminal cannot present a passkey, so the credential is minted where one works. The command prints a short code, opens the browser at the approval page, and waits. You compare the code, press Autorizar, and the terminal picks up a token of its own.
It works over SSH too: nothing depends on the browser being on the same machine, and
--no-browser prints the address instead of opening it.
The token is stored in ~/.config/fundaia/config.toml, owner-readable only. Revoke it from
Línea de comandos in the web interface — logout only forgets it locally, and says so.
What it does
fundaia status # every project, in one screen
fundaia services fisiovista # what is in one project, and what depends on what
fundaia service fisiovista api # one service in detail
fundaia project new Fisiovista # a project, which everything else hangs from
fundaia project rename fisiovista "FisioVista" --description "…"
fundaia project move fisiovista equipo # hand it to a shared space
fundaia project archive fisiovista --confirm fisiovista
fundaia deploy fisiovista api # deploy, and watch the pipeline
fundaia deploy fisiovista api --restart # reuse the image, restart the container
fundaia rollback fisiovista api 41 # put deployment #41 back
fundaia stop fisiovista api # sleep now; the next request wakes it
fundaia logs fisiovista api -f # follow the live output, in colour
fundaia logs fisiovista api -n 500 --stream build
fundaia history fisiovista api # the deployments, newest first
fundaia metrics fisiovista api # CPU and memory, now and over an hour
fundaia vars list fisiovista api
fundaia vars list fisiovista api --reveal
fundaia vars set fisiovista api JWT_SECRET … --secret
fundaia vars set fisiovista api JWT_SECRET … --sealed # nunca legible, ni una vez
fundaia vars seal fisiovista api JWT_SECRET --confirm JWT_SECRET
fundaia vars unset fisiovista api JWT_SECRET
fundaia vars import fisiovista api .env --secret # the whole file, in one request
fundaia volumes list fisiovista postgres
fundaia volumes add fisiovista postgres /var/lib/postgresql/data
fundaia volumes remove vol_abc123
fundaia recipe show fisiovista api # the Containerfile it builds with
fundaia recipe show fisiovista api --plain > Containerfile
fundaia recipe edit fisiovista api --file Containerfile
fundaia backups list fisiovista postgres
fundaia backups new fisiovista postgres # una copia ahora, y espera a que esté
fundaia backups schedule fisiovista postgres daily,monthly
fundaia backups restore bkp_abc --confirm bkp_abc
fundaia backups keep bkp_abc # que la retención no se la lleve
fundaia domains list fisiovista api
fundaia domains add fisiovista api app.midominio.com
fundaia expose fisiovista postgres --off # off the internet; siblings still reach it
fundaia expose fisiovista api # publish it again
fundaia templates # the components on offer
fundaia new fisiovista Postgres --template postgres-16
fundaia new fisiovista Claude --template claude-code
fundaia new fisiovista api --repo owner/api --connect-to Postgres,Storage
fundaia connect fisiovista api Postgres # wire an existing pair together
fundaia disconnect fisiovista api Postgres # and undo it
Everything the web interface can do, this can do, with three exceptions that only make sense with a
mouse: moving a node on the canvas, the live event stream, and the metric graphs — metrics prints
the numbers instead.
Projects and services are named however you can see them — the id, the slug or the display name all work. An ambiguous name is an error rather than a guess.
Backups
Every service with a volume is copied daily by default — including the ones that existed before this arrived. A backup nobody turned on is the only kind that is there when it is needed.
fundaia backups schedule fisiovista postgres daily,monthly
Three schedules, any combination of them, each with its own retention: daily kept for six days, weekly for a month, monthly for three. Passing none turns automatic copies off and leaves the existing ones alone.
What is copied is the volume, not a pg_dump. That is the same operation for Postgres, Redis
and a bucket, and it does not need the platform to know what is inside one. The service keeps
running while the copy is taken.
Restoring overwrites the volume, so it asks for the id back:
fundaia backups restore bkp_abc --confirm bkp_abc
The server takes a copy of the current state first, then stops the service, restores, and starts it again. That safety copy is not optional: the restore that goes wrong is the one where somebody picked the wrong row, and without it the state they replaced is gone.
backups keep pins a copy so retention will not delete it — what you reach for when you know a bad
migration is coming.
Sealed variables
A secret can be read back by whoever can reach the instance — that is what --reveal is. Sealing
takes that away, permanently:
fundaia vars seal fisiovista api STRIPE_SECRET --confirm STRIPE_SECRET
The container keeps being handed the value. Nothing else can have it: not this tool, not the web interface, not the API. The row cannot be overwritten either, so the key cannot be quietly pointed at a different credential while everything downstream keeps reading the same name. Deleting it is the only operation left.
The key has to be typed back because it cannot be undone. --sealed on set does the same in one
step, and is the only way a value is never readable at any point — sealing afterwards leaves it
readable for as long as it takes to run the second command.
What it does not defend against is somebody who can deploy code into that project: a container can print its own environment, and a sibling service can still reference the variable. It defends against the platform's own surfaces — a screenshot, a shared terminal, a stolen session — and against changing a credential by accident.
Private services
Every service is always reachable inside its project by name — that part is not optional and never was. What is optional is the hostname on top of it:
fundaia expose fisiovista postgres --off
The database keeps running, api keeps reaching it at postgres:5432, and nothing outside the
machine can. Its domains are removed rather than kept "for later": a hostname that still resolves is
the opposite of what was just asked for. Publishing it again gives it a hostname on the next
deployment.
Deploying, watched
deploy is the reason this exists rather than a curl alias. It starts the deployment and stays,
redrawing the six stages in place while the build scrolls underneath:
→ fisiovista api despliegue #42
✔ En cola 120ms
✔ Descargando el código 1.4s
▸ Construyendo la imagen …
○ Arrancando el contenedor
○ Comprobando que responde
○ En marcha
The stages come from the server, which is why they are the same six the web interface draws. Where it stopped has one answer, whichever window you are looking at.
It exits non-zero when the deployment fails, so deploy && smoke-test behaves.
Wiring services together
--connect-to and connect write references, never copied values:
DATABASE_URL becomes ${{ Postgres.DATABASE_URL }}, resolved at deploy time. Rotating the
database password changes one value and every consumer follows — and the reference is what draws
the arrow on the canvas, so the picture cannot drift from what the containers are handed.
What each kind of provider hands over is decided by the server, not here: connecting to a bucket
gives the S3_* set, connecting to another application gives its internal address and port.
Components
templates lists what this instance offers your account, and new --template <key> adds one:
claude-code is an agent a sibling service can ask questions of, and only the owner of an
instance may add one — it spends money and runs what it is told. An instance answers a stranger
asking for it exactly as it answers one asking for a component that does not exist, on purpose: what
it offers somebody else is not a stranger's business. So the tool says both things that answer can
mean rather than printing a 404:
✖ el componente `claude-code` no está disponible: o no existe en esta instancia, o solo su
dueño puede añadirlo — `fundaia templates` lista los que esta cuenta sí puede crear
In CI
Every option reads an environment variable, so nothing has to be written to disk:
env:
FUNDAIA_URL: https://infrastructure.fundaia.com
FUNDAIA_TOKEN: ${{ secrets.FUNDAIA_TOKEN }}
run: fundaia deploy fisiovista api
Colour turns itself off when the output is not a terminal; --no-color forces it.
Profiles
--url and --token on their own override whatever is saved, for one invocation, without
disturbing it.
Updates
The tool updates itself. When crates.io has a newer release it says so on stderr, never on
stdout — deploy is meant to be chained and its output is read by scripts — and then downloads that
release's binary for your platform, verifies its sha256 and moves it over the one you are running.
The command you typed finishes first; the next one is the new version.
! Hay una versión nueva de fundaia: 0.4.0 → 0.4.1
descargando…
✔ Actualizado. La siguiente orden ya usa 0.4.1.
It never gets in the way of what you asked for. The look at the registry runs alongside the command
rather than before it, the answer is cached for a day next to the profiles
(~/.config/fundaia/update-check.toml), and anything that fails — no network, a captive
portal, crates.io down, a binary somebody else owns — prints a line and leaves what is installed
alone. Nothing here can make a command that worked exit non-zero.
It only replaces itself in front of a person. With stderr on a terminal this is your laptop and the next command should be the new version; through a pipe it is a script or a CI job, where a binary that changes underneath a running pipeline is a bug nobody will find. There it prints the notice and stops.
Two environment variables, for the two different objections:
FUNDAIA_NO_SELF_UPDATE=1 # keep the notice, never install — your package manager owns this binary
FUNDAIA_NO_UPDATE_CHECK=1 # no notice, no registry, nothing
To do it now rather than on the next command:
upgrade ignores the cache, talks on stdout and fails loudly — somebody who asked to be updated and
was not is owed the reason.
The checksum is not optional, unlike in install.sh. That script runs because you typed it and can
read what it says; this runs by itself, over the binary that holds your deploy token, so a release
with no .sha256 beside it fails the update instead of being installed on trust.
Building from source
Releasing
Nobody edits the version in Cargo.toml. A push to main that changed anything under cli/ runs
.github/workflows/cli.yml, which reads the conventional-commit type of the commits that touched
this directory — feat a minor, a ! or a BREAKING CHANGE footer a major, anything else a
patch — raises the manifest and the lockfile, commits, tags cli-v<version>, builds the four
binaries and offers the crate to crates.io. A feat anywhere else in the repository reaches none of
it.
The release does not wait for that tag to start a second workflow, because it cannot: the commit and
the tag are pushed with github.token, and a push made with that token triggers nothing. release
and publish read the version off the job that decided it, in the same run. The workflow says why
at length, next to the job.
A version is never published twice. The number is derived by raising the one on main, and the
release commit moves that starting point on; a tag that already exists stops the run before it
commits; the publish step asks crates.io whether the version is there and skips it if it is; and
cargo publish refuses a duplicate on its own. If a target fails to build after the tag is already
pushed, gh workflow run cli.yml --ref cli-v0.4.0 runs the release again from the tag, and the
publish step skips whatever already landed.
crates.io needs CARGO_REGISTRY_TOKEN, and this repository has it. The token is scoped to
publish-new and publish-update on the fundaia crate and to nothing else — it cannot yank
a version, cannot change who owns the crate, and cannot touch any other crate on the account. It
does not expire, because a credential that lapses on a date nobody wrote down turns into a red
release three months after the person who minted it stopped thinking about it.
Should it ever be missing, the publish step says it is skipping and ends green: the tag, the four
binaries and their checksums are attached regardless, so install.sh keeps working and only
cargo install fundaia does not. To mint a replacement, go to Account settings → API
tokens on crates.io and add it here under Settings → Secrets and variables → Actions.
Licence
MIT.