Uninstall — completely removing Adjoint
This page is the complete removal story: every piece of Adjoint that exists
anywhere — AWS infrastructure, control-plane records, the CLI, and local state —
and the order to remove them in. Order matters: your local
~/.config/adj directory holds the OpenTofu state that deprovisioning needs,
so it must be deleted last, after the AWS infrastructure is gone.
The full sequence:
- Destroy instances (optional — deprovision destroys them anyway)
adj cluster deprovision— destroy the AWS data plane (per cluster)adj cluster rm— delete the cluster's control-plane record (per cluster)- Sweep AWS for known residue (older versions leaked FSx backups)
- Clean up control-plane records — sources, then the org itself
- Remove the
adjbinary - Delete
~/.config/adj— only now, never earlier
1. Destroy instances
$ adj ls
$ adj destroy <instance-id>
Strictly optional before a full teardown — adj cluster deprovision destroys
every instance along with the cluster. Destroying them first matters only if
you want clean per-instance audit entries, or if you're planning to use
adj cluster rm on a cluster you are not deprovisioning (rm refuses with a
409 while any instance is registered).
2. Deprovision each cluster (destroys AWS infrastructure)
$ adj cluster ls
$ adj cluster deprovision <name>
This DESTROYS the AWS data plane (EKS + FSx + all clones) for cluster <name>. Proceed? [y/N]: y
This runs an OpenTofu destroy locally, deleting the EKS cluster, the FSx for OpenZFS filesystem, and every clone on it. Requirements and caveats:
- You don't need the original machine. The control plane is the durable
home of a cluster's tfstate (encrypted at rest) —
deprovisionpulls the latest state from there before running the destroy, so any org admin, from any machine, can tear a cluster down. The~/.config/adj/provision/bundles/<cluster-id>/directory is now just a local cache. See Where provisioning state lives for the full lock/sync story. - Needs
tofuandawslocally, with credentials for the data-plane account. - If you originally provisioned with
--module-dir, deprovision with the same--module-dir. - Break-glass: pass the cluster UUID instead of a name and no login is required — teardown works even if the control plane is unreachable or your session is gone (it proceeds local-only from whatever state is on this machine, with a loud warning).
- Only one admin can run
deprovision(orprovision) against a given cluster at a time — a concurrent attempt is refused with the current holder's identity. See Operation locking.
Repeat for every cluster in adj cluster ls.
If you've lost the provisioning state
This is now the genuinely-lost case only — it does not apply just because the original provisioning machine is gone. Since state custody moved to the control plane, the primary recovery path for a missing local bundle is simply:
$ adj cluster deprovision <name>
run from any machine with an org-admin login — it pulls the control-plane copy of the state automatically. Try this first, always.
You're in the truly-lost scenario only if both of the following are true:
- The tfstate predates this feature and was never subsequently pushed
(check
adj cluster status <name>— aState:oflocal-only (never pushed)orunknownmeans there's nothing to pull), and - The machine that held the local bundle is gone, or its
~/.config/adj/provision/bundles/<cluster-id>/directory was deleted.
Only then, with no tofu state anywhere, delete the infrastructure directly
in AWS as a last resort, in this order: the EKS cluster (and its node
groups), then the FSx for OpenZFS filesystem, then any VPC resources the
module created for it. adj cluster bundle export <dir> shows you the
module's HCL, which doubles as an inventory of what was created.
3. Deregister each cluster
$ adj cluster rm <name>
Deletes the cluster's control-plane record. Pure bookkeeping — touches no AWS infrastructure — and returns a 409 while any instance is still registered (deprovisioning removes them).
4. Sweep AWS for known residue
An OpenTofu destroy removes what tofu created, but two classes of leftovers are known:
- FSx final backups. Teardowns run with
adjversions before theskip_final_backupfix left oneUSER_INITIATEDFSx backup behind per teardown. Check FSx → Backups in the data-plane account and delete any associated with the removed filesystem. (Current versions skip the final backup; older leaks stay until you delete them — they cost storage.) - Parked import volumes and golden snapshots. Import runs park their
Kubernetes PVs as
Retain— these are FSx OpenZFS child volumes (not EBS; nothing in the import or clone path uses EBS), kept because the golden's FSx snapshot is a child of the import volume and FSx refuses to delete a volume with snapshot children. A fulldeprovisiondestroys the whole FSx filesystem including these; they only linger if you tore things down by hand. Check FSx → your filesystem → Volumes/Snapshots — not EC2 → Volumes — for leftovers.
After the sweep, confirm with a cost check: the data-plane account should show no EKS, FSx, or orphaned EBS/backup charges for the region.
5. Control-plane records — what you can remove yourself
You can remove or disable most org content yourself:
$ adj catalog delete <golden> # golden snapshots (metadata)
$ adj pool rm <pool> # clone pools
$ adj users remove <email> # members
$ adj users disable <email> # or just disable
$ adj source rm <id> # source registrations
Sources and the org itself are also self-service:
Removing a source
$ adj source rm 3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b
Remove source 3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b (prod-replica.abcdef.us-east-1.rds.amazonaws.com/app)? Import history is kept but unlinked. [y/N]: y
Source 3e6f0a2b-7c8d-4e1f-9a2b-3c4d5e6f7a8b removed.
Credentials Secret adjoint-import/adjoint-src-app deleted from the data-plane cluster.
Admin-only. Prompts for confirmation unless you pass --yes. Refuses with a
409 while any import job for that source is still running or pending (the
error names the blocking job id(s) — wait for them to finish, or check with
adj import get <id>, then retry). Past import history is kept: a
removed source's jobs show - for source in adj import get, they just
lose the source link.
On success, adj source rm best-effort deletes the source's credentials
Secret from the data-plane cluster (namespace adjoint-import). If another
source registration shares the same Secret name (a reused --creds-secret,
or a colliding default adjoint-src-<dbname>), the Secret is not
deleted — you'll see a warning naming the other source(s) still referencing
it, plus the manual kubectl delete secret command to run once none do.
Deleting the org
$ adj org delete
You are about to permanently delete org "Acme Corp".
This cannot be undone: all users, catalog, sources, and the audit log will be deleted.
Password: ********
Type the org name (Acme Corp) to confirm: Acme Corp
Final audit export written to Acme Corp-audit-final.jsonl
Chain head: 3f9a1c7e2b8d4f0a6c5e9b2d1a8f7c3e0b6d4a9f2c1e8b7d3a0f9c6e2b1d8a4f
Org deleted. Your session is now invalid.
adj org delete is interactive only — there is no --yes/-y flag,
because this is irreversible. It prompts for your password (no-echo, fresh
re-auth against your own account) and then the org's exact display name
(shown in the warning — this is the org name, not the slug) before doing
anything. Admin-only.
Precondition: zero registered clusters. If any cluster is still
registered, the command refuses with a 409 listing them by name and id, plus
the required order — deprovision each (adj cluster deprovision), then
deregister it (adj cluster rm) — same as steps 2–3 above.
On success:
- The final audit export — every event this org ever recorded, plus the
deletion itself — downloads automatically to
./<sanitized-org-name>-audit-final.jsonl(mode0600). Archive this file immediately: the download URL is single-use and expires in 15 minutes; if the automatic download fails, the command prints the URL instead so you can fetch it by hand within that window — after that, or after one successful fetch, it's gone for good. - The export's last line carries the audit chain's head hash plus a signature keyed with a server-held secret. You cannot independently verify that signature yourself — treat the export as Adjoint's attestation of the final state, not a customer-verifiable proof. The chain head hex is also printed to your terminal for archival cross-reference.
- Every org token — yours and everyone else's — is invalidated immediately;
the command also clears your local session. Any further API call with an
old token gets a
401, not a second delete. - Users, invites, sessions, catalog, sources, import history, clone pools, quotas, org policy, and the audit log are all deleted, replaced by a single anonymized tombstone record (hashes only — no names, emails, or content survive).
- The org's name and slug are free to reuse afterward — nothing about a deleted org blocks a fresh signup with the same name.
If you'd rather not run this yourself, or need help recovering from a partial teardown, email [email protected] from an org-admin address.
6. Remove the CLI binary
The installer placed adj at /usr/local/bin/adj, or ~/.local/bin/adj if
/usr/local/bin wasn't writable:
$ which adj
/usr/local/bin/adj
$ sudo rm /usr/local/bin/adj # or: rm ~/.local/bin/adj
7. Delete local state — last
$ rm -rf ~/.config/adj
(Respect $XDG_CONFIG_HOME if you've set it: the directory is
$XDG_CONFIG_HOME/adj.) This removes your session token, CLI config, and the
provisioning bundles including the OpenTofu state. That state is the only
record capable of destroying your AWS infrastructure — which is why this step
is last. If you delete it while a cluster still exists, you're in the
"lost the provisioning state" scenario above: manual AWS deletion.
Checklist
-
adj cluster lsshows no clusters (each one deprovisioned, thenrm'd) - FSx backups + orphaned EBS volumes swept in the AWS console
- AWS cost explorer shows no residual EKS/FSx/EBS charges
-
adj org deleterun (if you want the control-plane records gone) — audit export archived somewhere durable -
adjbinary removed -
~/.config/adjdeleted — after, and only after, all of the above