How Flui reads a repository
There are three ways to get from a repository to a running
application on Flui, and none of them ranks above the
others. You can write flui.yaml yourself. You can bring an
image you already built. Or you can ask Flui to read the
repository and tell you what it found. That result is called
a map, and it is the subject of this chapter.
A tool that reads your code and writes a deploy configuration for you can easily become a black box: it says “deployable” and you either trust it or you don’t. Flui avoids this by never letting the map say more than it can back up. Every fact it reports carries where it was read and how sure the engine is of it. Where it could not tell, it says so and asks instead of picking a default.
What gets read
Flui reads a repository as one in-memory archive of a single commit: no clone, and nothing left on disk once the read ends. Symlinks are refused, never followed. The read runs under ceilings: how much of the archive it will decompress, how large a single file it will look inside, how many paths it will list, and how long the whole read may take. Every ceiling is reported back in the response, together with whether the read hit one. A fact missing because the read stopped at a ceiling is different from a fact that was looked for and not found, and the response says which one happened.
What comes back
Producing a map is read-only: the route is
POST /repositories/:id/map, and calling it touches nothing
on the repository or on a cluster. A map holds:
- Units: the directories Flui would deploy, each with the reason it counts as one (its own Dockerfile, a compose service, the repository root).
- Services: the databases, caches and queues the repository seems to want, each matched against a catalog block where one exists.
- Inputs: the values you would have to supply, secret or not, before a unit can start.
- Externals: dependencies on things outside Flui’s catalog, such as a third-party API or a managed service the repository was written to expect.
- Blockers: things Flui cannot provide, such as a Docker socket mounted in, a build that wants a private-registry secret at build time, or a GPU reservation.
- Caveats: cases where the unit runs, degraded. For example, an embedded SQLite on what Flui would put on shared storage, or two composition files that might declare the same database twice.
- Open questions: where the evidence does not settle something on its own.
- Decisions: a choice made on your behalf where more than one reading was possible, with the reason and the alternative.
For every unit the render could produce a manifest for, the
map also holds one rendered flui.yaml: the same kind of
file you would write by hand, generated from what was read.
A citation and a confidence, on every fact
Every entry (a port, a service, a blocker) carries an
evidence list: the file, the line where there is one, and
the exact token that was read. You can open the file at that
line and check it yourself.
Every entry also carries a confidence: declared (the
repository states it outright, such as an EXPOSE in a
Dockerfile or a DATABASE_URL read straight out of
docker-compose.yml), derived (inferred from something
else that is itself declared), or circumstantial (a weaker
signal, such as an import or a naming convention).
Corroboration never promotes a reading: three circumstantial
signals pointing the same way are still circumstantial.
A verdict, always, from a closed list
Every map ends in a verdict, one of six: deployable,
deployable_pending_inputs, partial, blocked,
insufficient_capacity, not_assessed. There is no seventh
answer.
The verdict depends on the repository and the cluster
together, not on the repository alone. Ask for a map with no
clusterId and you get only the repository half (each
unit’s own readiness, and whatever blockers or caveats
apply) with verdict.capacity.assessed: false and the
reason. A deployable that was never weighed against a real
cluster must never read as one that was. Name a cluster and
the map also adds up the CPU and memory every unit and every
matched service would request (a catalog block’s own
declared resources, or the platform default for a unit that
declares none) and compares the total with what that cluster
has free, using the same safety margin as the rest of the
platform’s capacity gate. Anything that would run but could
not be weighed (a service with no catalog block behind it,
say) is named in uncounted rather than silently counted as
free.
coverage on a map always reads best_effort. The engine
reserves supported for accuracy earned by measurement,
never for structure alone, so no map claims it, whatever
else it reports.
Where it stops instead of guessing
Open questions are expected. Where the evidence does not settle something (which of two declared databases is the real one, or whether a route the repository exposes is a health check), the map says so, names the readings the evidence supports, and leaves the choice to you.
A blocker is something Flui has no way to provide: a Docker socket mounted into the container, a build that needs a secret at build time, a kernel capability the platform does not expose. A blocked unit always carries a remedy.
One blocker applies to the repository as a whole rather than to a unit: more than five deployable applications. Past that number, the reader stops instead of reading each one on its own files, and the verdict says so by name. It lists the roots it found, says why each was promoted, and asks you to name at most five and map them separately, or to split the repository. It does not rank them for you: on a workspace that large, any ranking it could apply would recommend a test harness or a build helper with the same confidence as an application.
Acting on the map
Reading a map changes nothing. Acting on one is a separate
call, POST /repositories/:id/map/apply, and its scope is
narrow.
Flui cuts its own branch, flui/deploy-<sha7>, at the exact
commit the map was read from, not at the tip of your branch,
which may have moved since. Flui reads from your branch and
cuts from it, but never writes to it. One commit lands on
the new branch, carrying every applied unit’s rendered
flui.yaml and a build workflow beside it, and that single
commit starts every build. That is why the apply lands as
one commit and not one per unit: several commits would mean
several builds racing each other over the same webhook.
If a unit’s rendered manifest declares deploy.services (a
database, say), that service is installed from the catalog
and Flui waits for it to reach RUNNING before anything is
written to GitHub. The application row and its database both
exist before the commit lands; the commit starts the build
on top of them. An apply refuses on a verdict of blocked,
insufficient_capacity or not_assessed, since spending
your repository’s Actions minutes to reach a conclusion the
map already reached for free would be pointless. You can
also apply only some of the units (--unit, repeated, on the
CLI; unitIds on the API and MCP) instead of the whole map.
Because an apply provisions real things (databases, applications) before it touches GitHub, a failure partway through can leave something behind. If one unit of several cannot be prepared, Flui deletes the branch it cut, so your repository stays untouched, but the units prepared before the failure already exist, with whatever they attached already running. Flui names them in the error instead of deleting them, and the next apply of the same repository reuses them instead of creating a second copy.
This is also the one place where the time an apply needs and
the time a caller is given can disagree. Waiting for a newly
installed database to reach RUNNING takes minutes, which is
longer than an MCP tool call lasts. See The Flui assistant
and MCP for what
happens when the two collide, and why the CLI does not have
this problem.
Three paths, at parity
The map is one of three ways to reach a running application, and Flui’s own tools state this: reading the repository is “ONE way to reach a deploy and not the required one,” and bringing an image you already built “carries the same standing as the two routes that go through it.” None of the three is a fallback for the others:
- The map: Flui reads the repository and renders the manifest for you.
- Your own manifest: you write
flui.yamlby hand;flui deployorapp_deploy_from_yamltakes it from there. - An image already built: the dashboard’s Deploy New →
From a Docker image, or
app_deploy_imageover MCP, skips the repository and the build entirely.
See Deploy an application for the map and the hand-written manifest side by side.
A limit: two units, two databases
deploy.services attaches a building block to one
application. Ownership is recorded per application: an
install belongs to the row that asked for it, and nothing
else can claim it.
That works for a single application, but it leaves a gap for a monorepo. Two units that each declare the same database (a shared Postgres that two services in one repository both want to use) are two separate applications once deployed, so each gets its own database. No field in the manifest says “this is the same instance as that other unit’s”: the spec does not carry one, and the render does not emit one. If your repository’s units are meant to share a database, wire that by hand after the apply.
Getting to it
- CLI:
flui repo map showandflui repo map apply, in Integrations and repositories. - Dashboard: Repositories, then Readiness map on the repository you want to read.
- API / MCP:
repo_mapandrepo_map_apply, alongsideapp_deploy_image, in The Flui assistant and MCP.
Where this chapter goes from here
- Deploy an application: the map and the hand-written manifest, walked end to end.
- The Flui manifest: what
a rendered
flui.yamlcan say, and whatdeploy.serviceswires up once it lands. - The Flui assistant and MCP:
repo_map,repo_map_apply, and the safety model every write on this engine goes through. - The action cycle: why
repo_map_applystops and asks a person before it commits anything. - The build pipeline: what the workflow an apply commits does.