Integrations: pushing values to GitHub Actions, Coolify, and Convex
An integration pushes one environment’s values to a platform that keeps its own copy: the Actions secrets of a GitHub repository, the environment variables of a Coolify application or service, or those of a Convex deployment. After that, every change and rotation in the environment reaches the platform without anyone re-entering it.
You set one up in two parts:
- A connection holds one credential for one platform account or instance. Create it once, under Connections or while adding an integration. Several integrations can share it.
- An integration pushes one environment to one destination through a connection. Add it on the environment’s Integrations tab: pick the connection, the destination, the items, then review what will leave Varlatch.
Managing connections and integrations needs config.sync.manage. Adding an
integration also needs the authority to disclose what it pushes:
secret.reveal for Secrets and config.value.read for the other items
(both, on the whole environment, for “All items”). The installation must
allow outbound sync; see VARLATCH_SYNC.
Varlatch checks the credential first
Section titled “Varlatch checks the credential first”Before it saves anything, Varlatch asks the platform whether the credential works: when you create a connection, when you replace its credential (once for every integration that uses it), and on the review step of a new integration, against the destination you chose. When the check fails, the dialog says what is wrong and which part to fix. You can still go ahead (Save anyway, Create anyway), for example for a GitHub environment you have not created yet.
A platform cannot show whether a credential may write without a write, so a passed check means Varlatch can reach the destination and read it; the first push confirms the rest. If it fails, the integration says so, and the connection shows Credential rejected with a Replace credential button.
GitHub Actions
Section titled “GitHub Actions”A GitHub connection authenticates in one of two ways:
- Through your organization’s GitHub App (below). Varlatch issues a token for each push, limited to one repository and valid for an hour. Nothing expires with a person, and nothing breaks when someone leaves.
- With a personal access token, which a person creates and which can expire. It is quicker to set up, and works where you may not register Apps.
Through a GitHub App
Section titled “Through a GitHub App”Each Varlatch organization has at most one GitHub App, registered on the GitHub account it pushes to. Varlatch keeps the App’s private key, encrypted, and stores no token: for each push, check, or listing, it asks GitHub for a token limited to that use.
| Use | Repositories | Permissions |
|---|---|---|
| Push | the destination | Secrets (or Environments) read and write, Metadata read |
| Check a destination | the destination | Secrets (or Environments) read, Metadata read |
| Check the connection, list repositories | the whole installation | Metadata read |
Register the App. Under Connections, GitHub App, Register an App: choose the GitHub organization (or your personal account) and press Continue to GitHub. GitHub shows its form to create the App, filled in by Varlatch: secrets and environments (read and write) and metadata (read), no webhook, private. Press Create GitHub App, and GitHub sends you back to Varlatch.
- On an organization, only an owner, or a member allowed to manage its GitHub Apps, can register one. If GitHub says you don’t have permission and offers to create the App for your own account, stop there: that App can only be installed on your account, not on the organization. If it happens anyway, Varlatch keeps nothing, says so, and links to where you delete the App on GitHub.
- Registering needs this installation’s public address over HTTPS
(
VARLATCH_PUBLIC_URL), or a loopback address in local development: GitHub sends the browser back there. - The link GitHub sends you back with works once, for an hour. Opening it again (a refresh, for example) says whether the registration finished and names the organization’s App when there is one: keep that App. Only when the page says the registration did not finish, and the organization has no GitHub App in Varlatch, may GitHub still have created one: delete that App on GitHub under Settings, Developer settings, GitHub Apps, then start again.
Or import one. If an owner registered the App, Import an App takes
its App ID and a private key (the .pem file GitHub downloads under the
App’s Credentials, Key pairs, New key). Varlatch checks the
pair with GitHub before keeping it. The App needs Secrets and
Environments (read and write) and Metadata (read). It may hold more, and
the audit log names the extra permissions, but the stored key keeps
everything the App holds: Varlatch limits each token it issues, not the
key. Give the App no more than it needs.
Install it, then connect the installation. On GitHub, install the App on the account (Install on GitHub links there) and choose the repositories Varlatch should push to. Back in Varlatch, Connect an installation creates a connection for it. The connection’s owner is the installation’s account, and its integrations can push only to the repositories the installation includes. To push to another repository, add it to the installation on GitHub; if you are not an owner there, GitHub sends your change to an owner to approve, and the repository is listed once it is reachable.
Rotate the key. Under GitHub App, Rotate key: on GitHub, open
the App’s settings, Credentials, Key pairs, New key, then give
Varlatch the new .pem. Varlatch checks it with GitHub first, then
switches every integration on the App’s connections to it at once,
paused and disabled ones included. That is a new disclosure for each of
them, so you need the authority to disclose what each pushes; if you
lack it for one, nothing changes. Once Varlatch confirms, delete the old
key on GitHub. An App connection has no credential of its own: its
Rotate the App’s key button leads here. It never shows an expiry.
Remove the App. Remove App removes the App you confirmed (if another one replaced it meanwhile, nothing changes) and revokes every connection on it at once: their integrations are disabled and keep their destinations, and Varlatch deletes the key. The App stays on GitHub; delete it there, under the App’s Advanced settings, if you no longer need it.
The audit log records the App’s registration or import (and a refused registration, with why), each listing of its installations, each key rotation with the integrations it re-authorized, and its removal with the connections it revoked; never the key. A token Varlatch issues is not logged on its own: the push, check, or listing that used it is.
With a personal access token
Section titled “With a personal access token”| Field | What to enter |
|---|---|
| Owner | The user or organization in github.com/<owner>. |
| Access token | A fine-grained personal access token, below. |
| Repository | On the integration: pick it from the repositories the token can see in the owner, or type its name, without the owner. |
| GitHub environment | Optional. Empty writes repository secrets; a name writes the secrets of that environment, which must already exist in the repository’s settings. |
Create the token on GitHub under Settings, Developer settings, Personal access tokens, Fine-grained tokens, Generate new token:
- Resource owner: the owner you enter in Varlatch. An organization can require approving the token before it works.
- Repository access: Only select repositories, and pick the ones you push to.
- Repository permissions: Secrets: Read and write for repository secrets, Environments: Read and write for environment secrets. GitHub adds Metadata: Read-only itself.
- Expiration: when the token expires, pushes stop with “Credential rejected” until you replace it under Connections. Varlatch shows the date GitHub reports: in the check while you create or replace the credential, and on the connection once it has used the token. From two weeks ahead it warns on the connection and on every integration that uses it, and a dialog asks before saving a token that expires within two weeks. A token without an expiry gets no date: GitHub reports none.
To push to another repository later, add it to the token’s repository
access on GitHub; the token stays the same, so use Check again on the
review step instead of replacing it. A classic token with the repo scope
works too, but it reaches every repository you can.
GitHub never returns secret values, so Varlatch cannot read them back; its periodic repair writes them again instead.
Coolify
Section titled “Coolify”| Field | What to enter |
|---|---|
| Instance URL | The address of your Coolify instance, with https:// and nothing after the host: https://coolify.example.com. |
| API token | A Coolify API token, below. |
| Application or service | On the integration: an application, or a service (a one-click service or a Docker Compose stack), below. |
| Application UUID | For an application: pick it from those of the token’s team, or paste the last part of its address in Coolify (…/application/<uuid>). |
| Service UUID | For a service: pick it, or paste the last part of its address in Coolify (…/service/<uuid>). |
| Build time | For an application: whether the variables exist during the build. Values compiled into a build (VITE_*, NEXT_PUBLIC_*) need it, and then end up in image layers. |
| Names Coolify resolves | Optional, for an application: names whose values point at Coolify itself, such as {{team.GITHUB_TOKEN}} or $SOURCE_COMMIT. See Values Coolify resolves. |
Then, in Coolify:
- Turn on API access if it is off: Settings, Advanced, API and MCP. If Allowed API IPs is set, add the address Varlatch’s server connects from.
- Create the token under Keys & Tokens, API Tokens, logged in as an admin or owner of the team that holds the application. Coolify limits team members to read-only tokens.
- Give it the Read and Write permissions, and Deploy when the integration redeploys after changes. Check the selected permissions before you create it: in some versions, ticking Deploy clears the others.
- Add Read sensitive if Varlatch should read values back. Without
it, Coolify lists variables without their values: Varlatch’s periodic
repair then writes every value again instead of fixing only what
changed,
varlatch sync checkreports those values as not compared, and a service’s compose file cannot be checked (below).
A Coolify token reaches every application and service of its team, so one connection per instance is the honest unit. Create a token for Varlatch alone, so you can revoke it on its own.
Services
Section titled “Services”A Coolify service runs several containers from one compose file, and its
variables belong to the whole service: Coolify writes them into one .env
that every container loads, its bundled databases included. An integration
with a service therefore discloses each value to every container of the
service, and to any container added later; the review step lists today’s.
- Names Coolify sets itself cannot be pushed to a service:
SERVICE_FQDN_*,SERVICE_URL_*,SERVICE_NAME_*, andCOOLIFY_*. An explicit list that names one is refused; with All items, exclude them. A name the compose file binds to a Coolify value (DB_PASSWORD: ${SERVICE_PASSWORD_POSTGRES}) is Coolify’s too: the integration reports it and never writes or deletes it. - Passwords Coolify generated (
SERVICE_PASSWORD_*,SERVICE_USER_*and the like) may be pushed. A database usually reads its password only when it first starts, so changing one later can cut the link between the database and the containers that use it. - Restart after changes restarts the whole service, its databases included; Coolify queues it. It is off unless you turn it on.
- Removing a name deletes the variable once. When the compose file references the name, Coolify creates it again at its next start (with the compose file’s default, or empty), and Varlatch leaves it there rather than delete it on every repair. It does the same when it cannot read the compose file. The integration card notes such names.
With Read sensitive, Varlatch also reads the service’s compose file on every push and notes, on the integration card, what it finds for the names it pushes: a container whose compose file sets the name to another value, which wins over the pushed one; a name no container uses; or a form it does not read. These are hints from the compose file, not proof of what a container does with a value, and they never fail a push. Only names, container names, and these verdicts leave the compose file; Varlatch stores nothing else from it.
Values Coolify cannot carry
Section titled “Values Coolify cannot carry”Varlatch writes every value to Coolify as literal and multiline, so
Coolify puts it in single quotes in the .env it deploys instead of
expanding $ or cutting it at #. A variable Varlatch manages that is
stored without those settings (one written before, or by hand) is written
again on the next periodic repair, even when its value already matches.
What that format cannot carry, or what Coolify would change on the way,
is not pushed, even when Coolify already holds that value: the integration
reports the name and the reason, keeps what Coolify has, and the other
values still go.
- leading or trailing whitespace, a trailing line break included (Coolify removes it);
- a single quote, or a trailing backslash (they break the single-quoted
.env); {{team.…}},{{project.…}},{{environment.…}},{{server.…}}, or{{vault.…}}(Coolify replaces them with its shared variables; for an application, list the name for Coolify to resolve, below);- a JSON object or array containing
$,#, or a line break (Coolify writes JSON unquoted); - for a service, a
$, unless Varlatch read the service’s compose file (with Read sensitive) and found that it does not copy the value into a container’s settings, where Docker Compose expands it: a default (${NAME:-default}), another name’s value (OTHER: ${NAME}), an empty value, or a bare- NAMEin a list.
A name that is not pushed is no failure: the integration shows Values held, with each name, its reason and its fix, and keeps pushing the others. It does not stop, and does not retry on its own; a changed value, or the next periodic repair, checks it again.
Values Coolify resolves
Section titled “Values Coolify resolves”Some values are meant for Coolify to interpret: {{team.GITHUB_TOKEN}}
points at a team shared variable, and $SOURCE_COMMIT at the commit
Coolify builds. Varlatch cannot tell those from a password that happens to
contain $ or {{, so it never guesses. For an application, list the
names under Names Coolify resolves (or Let Coolify resolve it on a
held name): Varlatch then stores them the way Coolify stores a variable
created in its own interface, neither literal nor multiline, and Coolify
resolves them as it does its own. Every other value is still stored as
text.
What Varlatch promises for such a name is Coolify’s own interpretation of the stored text, nothing about the value Coolify puts in its place. It still refuses what would change on the way besides that interpretation:
- leading or trailing whitespace;
- a line break (the value is written unquoted, where it ends the line);
- a carriage return, a tab, a backslash, or a single or double quote (Coolify escapes them);
#(it starts a comment);- a JSON object or array (Coolify writes JSON as it is);
{{vault.…}}(a linked secret manager, which Varlatch does not cover).
Varlatch’s own references are expanded first, so to hand Coolify
${NAME}, write $${NAME}. A variable already stored that way is left as
it is; one stored as literal text is changed to Coolify’s form at the next
periodic repair (within six hours). Services do not take this option.
Convex
Section titled “Convex”| Field | What to enter |
|---|---|
| Deployment URL | https://<name>.convex.cloud for Convex Cloud, as shown in the deployment’s settings, or the origin of a self-hosted backend. |
| Deploy or admin key | Convex Cloud: a deploy key for that deployment, generated in its settings in the Convex dashboard. Self-hosted: the backend’s admin key. |
A key works for one deployment only, so each deployment is its own connection, and the deployment is the destination: the integration has nothing more to fill in. A production and a development deployment have different keys. The key must be allowed to change environment variables; a read-only key fails the check. A self-hosted backend from before March 2025 has no key check at all: the check then reports the deployment as not found, so update the backend, or save anyway.
If the environment also holds the deployment’s own key or URL
(CONVEX_DEPLOY_KEY, CONVEX_URL), keep them out of the push with All
items except CONVEX_*. Convex functions read new values at once; there
is no redeploy.
When the check fails
Section titled “When the check fails”| The dialog says | What to do |
|---|---|
| The credential was rejected | It is mistyped, expired, or revoked, or a Convex key belongs to another deployment. Paste it again or make a new one. |
| The credential is missing a permission | Give it the permissions listed above for its platform. On GitHub, also check that the organization approved the token; on Coolify, that API access is on and the allowed IPs include Varlatch’s server. |
| Account or instance not found | Check the owner, instance URL, or deployment URL. |
| Destination not found | Check the repository, GitHub environment, or application UUID, and that the token can see it. |
| Could not check right now | Varlatch’s server got no answer: the dialog says whether the address does not resolve, refuses the connection, redirects (a sign-in page in front of the API, for example), or has a certificate the server does not trust. A timeout, a rate limit, or a platform error clears with Check again. |
| The platform refused the check | Most often the address answers but is not the platform’s API: check the instance or deployment URL. |
| GitHub refused the App’s key | The key was deleted on GitHub, or belongs to another App: rotate the App’s key. |
| This server’s clock is ahead of, or behind, GitHub’s | GitHub refuses the App’s signed requests when the server’s clock is minutes off. Set it right (NTP), then check again. |
| The App’s installation cannot see the repository | Add the repository to the App’s installation on GitHub, or check its name. |
Picking the destination
Section titled “Picking the destination”On the Destination step, Varlatch lists what the connection can reach: for GitHub, the repositories in its owner that the token can see, or that the App’s installation includes, archived ones left out in both cases; for Coolify, the applications or the services of the token’s team. Pick one, or type it when it is not listed: a repository you have not created yet, or one a partial list leaves out (Varlatch stops reading at 1,000, or after 30 pages of GitHub repositories, and says so). If the list cannot load, the step says why, as a failed check does, and Try again reloads it; typing works meanwhile. After creating a repository or application on the platform, reload the list from the same step. Choosing another connection, or changing a new connection’s owner or address, clears the destination and loads the new list.
Seeing a destination is not permission to push to it: Review still checks the one you picked or typed, and the first push confirms it.
What a check sends
Section titled “What a check sends”A check only reads. It sends the credential to the platform and nothing else; a stored credential goes only to its connection’s own address. It reads nothing from the environment and stores nothing. An answer counts only when it has the platform’s own shape, so a sign-in page in front of an instance, which answers too, does not pass.
| Platform | Requests |
|---|---|
| GitHub Actions | GET /users/<owner>; with a destination, the Actions secrets public key of the repository or environment (and the repository itself, to tell a missing environment from a repository the token cannot see) |
| GitHub Actions, through the App | First a token for this check, from POST /app/installations/<id>/access_tokens (signed with the App’s key); then, for the connection alone, GET /installation/repositories?per_page=1, and with a destination, the same requests as with a token |
| Coolify | GET /api/v1/version; with an application, GET /api/v1/applications/<uuid>; with a service, GET /api/v1/services/<uuid> and GET /api/v1/services/<uuid>/envs (the request every push starts with: Coolify lets only team admins and owners manage a service’s variables). Values in the answer are discarded; the review names the service’s containers. |
| Convex | GET /api/check_admin_key |
Each check appears in the audit log as Access checked, with the
platform, the address or destination, and the outcome; never the
credential. Through the API, it is
POST /v1/organizations/{org}/platform-connections/check.
Listing destinations reads in the same way, with the same credential:
| Platform | Requests |
|---|---|
| GitHub Actions | GET /users/<owner>, then GET /orgs/<owner>/repos for an organization or GET /user/repos (filtered to the owner) for a user, 100 per page |
| GitHub Actions, through the App | A token for the listing, then GET /installation/repositories, 100 per page: exactly the installation’s repositories |
| Coolify | GET /api/v1/applications, then GET /api/v1/services; only each application’s uuid, name, and first address, and each service’s uuid and name, are kept |
A listing appears in the audit log as Destinations listed, with how
many it found and never their names. Through the API, it is
POST /v1/organizations/{org}/platform-connections/destinations.

