Organizations, folders, stacks
Firstlume organizes infrastructure as a tree: organization → folders → stacks. The tree belongs to you, not to the layout of your repositories. Settings defined high in the tree are inherited below, and any level can override them.
Organizations
An installation has many organizations. Each one has a display name and a slug, the identifier in URLs (lowercase letters, numbers and hyphens, 2 to 40 characters). All data (folders, stacks, runs, agents, variables, connections) belongs to exactly one organization, and nothing crosses between them.
Folders
A folder is a free-form node in the tree. Folders are optional: an organization can keep every stack at the top.
- Names have 1 to 100 characters, can't contain
/, and are unique among siblings, ignoring case. - Folders can nest up to 10 levels.
- A folder can be renamed. It can only be deleted when empty.
- Moving a folder isn't supported yet.
Stacks
A stack is one managed infrastructure. It sits in a folder or at the top of the organization, and it points to:
- a repository
- a branch, by default the repository's main branch
- a path: the folder in the repository that contains
.firstlume/config.yaml, such asenvironments/prod, or.for the root
environments/
├── prod/
│ ├── .firstlume/config.yaml → stack with path environments/prod
│ └── main.tf
└── edge/
├── .firstlume/config.yaml → stack with path environments/edge
└── main.tf
Several stacks can come from the same repository and branch, with different paths. Each one has its own state, lock, runs, variables and settings. Two stacks in an organization can't share the same repository, branch and path, since they would manage the same infrastructure with different states.
Deleting a stack doesn't destroy infrastructure. A stack with an active run, or with resources in its state, can't be deleted.
Moving a stack
A stack can move to another folder or to the top: drag it in the tree, or use "Move…" on its page. The UI first shows what changes in what it inherits. The move is refused while the stack has a run queued, planning, awaiting approval or applying, or if the destination already has a stack with that name. Runs, state and versions stay with the stack.
Inheritance
For each setting, the value comes from the first level that defines it, going up from the stack:
stack → its folder → parent folder → ... → top folder → organization
| Setting | API field | When no level defines it |
|---|---|---|
| Require approval before apply | require_approval |
required |
| Drift check interval | drift_interval_minutes |
off |
| Confirm each deletion or replacement | confirm_destructive |
off |
| Vault role | vault_role |
none |
| Terraform, environment and file variables | variables |
none |
| Registry credentials, combined by host | registries |
none |
The identity of a variable is its category plus its name, so region as a Terraform variable and region as an environment variable are different things. Secrets are variables with sensitive: true and follow the same rule. Their values are never returned by the API at any level.
You can override an inherited variable, but you can't remove it at a lower level.
Example
Organization Acme TF_VAR_region=us-east-1, secret DB_PASSWORD=a, approval=yes
└── Infra (nothing)
├── prod secret DB_PASSWORD=b, drift=every 6h
│ └── stack cluster (nothing)
│ → region=us-east-1 (Acme), DB_PASSWORD=b (prod),
│ approval=yes (Acme), drift=6h (prod)
└── dev approval=no
└── stack dev01 TF_VAR_region=sa-east-1
→ region=sa-east-1 (stack), DB_PASSWORD=a (Acme),
approval=no (dev), drift=off (default)
At any level, the UI shows what is defined there, what is inherited and from where, and what that level overrides.
When values are read
- The plan uses the effective variables at the moment it starts.
- The apply uses the same variables as the plan, from a snapshot stored encrypted in the run, even if someone changes a folder in between.
- The approval rule is read when the plan finishes.
Settings in the API
GET|PUT /api/v1/orgs/{org}/settings
GET|PUT /api/v1/orgs/{org}/folders/{id}/settings
GET|PUT /api/v1/orgs/{org}/stacks/{id}/settings
A
PUTreplaces what the level defines. Send the level's settings and variables each time. A setting left out goes back to inheriting. A sensitive variable sent with an empty value keeps its stored value. The one exception isregistries: without the field, the level's credentials stay as they are.
Git connection
An organization can connect to one Git account. Gitea is the first supported provider. In Gitea, the account is an organization or a user, and the credential is a service token from a bot account, with read access to the repositories. The token is separate from anyone's login, so nothing stops when someone leaves. It is encrypted and never returned by the API.
With the connection, Firstlume lists the account's repositories and finds the .firstlume folders in a branch without cloning. It marks each one as valid or invalid, and shows whether a stack already uses it. The "New stack" wizard is built on this.
Without a connection, a stack can use any Git repository by its clone URL, with an optional token.
Members and access
Every signed-in user with access to an organization is an administrator of it. Finer roles are planned.
Who sees what
| Who | Organizations they see | Can create organizations |
|---|---|---|
| Admin token | all | yes |
Installation administrator, a member of a group in auth.oidc.adminGroups |
all | yes |
| Any other signed-in user | only those they are a member of | no |
An organization you aren't a member of answers 404 on every route, including stacks, runs and state, so its existence isn't revealed. Someone with no organization sees a screen with their login, ready to send to an administrator, and no way to create one.
auth.oidc.allowedGroups only decides who can sign in. It gives no access to any organization: that takes an invitation, or being an installation administrator.
Invitations
Members and installation administrators invite people by their Gitea login:
curl -s -H "$H" $O/members -d '{"login": "maria"}'
- Someone who has signed in before becomes a member immediately.
- Someone who never signed in gets a pending invitation, which takes effect at their first sign-in. Logins are compared ignoring case.
- Pending invitations appear in the member list, can be canceled, and expire after 30 days. Firstlume doesn't check whether the login exists, so a typo shows up as an invitation nobody accepts.
- Removing a member takes effect immediately, even with a session open, and doesn't cancel runs they already started.
Runs, approvals and setting changes record who did them: the person's login, or admin-token.