Skip to Content
Operate EnergonAdminister Energon

Use /admin when you need to see platform storage health, cut off another account’s agent keys, or retire sites and files across this Energon. The page exists because ordinary hub cleanup only covers work the signed-in person can already write, while leftover tokens and quota drift keep failing publishes after someone leaves. Set ADMIN_EMAILS first, then mint an admin token from /keys. Connect never grants that scope.

Administer Energon

Who can open /admin

ADMIN_EMAILS is a comma list in deploy vars. Only those addresses can create an admin key (scope: admin) at /keys. Admin keys last at most 7 days, default 1 day, and cannot be never. Admin routes also check that the owner is still on the list, so removing an email strips admin immediately. Ordinary /v1 calls with that key still act as the account.

Put /admin on the Access allow list with the other hub pages. An account token calling /v1/admin is 403 forbidden_admin.

Repair storage health

/stats already shows used versus MAX_PLATFORM_BYTES to every signed-in user. /admin adds pending-purge counts, stale purge claims, locked share gates, and three repairs that do not delete live work:

  1. Recompute the quota ledger from stored sizes. From v1.10.0, the recompute keeps charges for open deployments, retained old versions, and cleanup that has not finished.
  2. Run one expiry sweep. If remaining is greater than zero, run it again. From v1.10.0, the sweep also advances enabled site conversions.
  3. Unlock a share gate by scope (obj:… or ip:…). After too many bad passwords, unlock both.
export ENERGON_ORIGIN="https://hub.your-company.example" test -n "$ENERGON_TOKEN" || { echo "Set an admin ENERGON_TOKEN" >&2; exit 1; } curl -fsS "$ENERGON_ORIGIN/v1/admin/health" \ -H "Authorization: Bearer $ENERGON_TOKEN" curl -fsS "$ENERGON_ORIGIN/v1/admin/quota/recompute" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" curl -fsS "$ENERGON_ORIGIN/v1/admin/sweep" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" curl -fsS "$ENERGON_ORIGIN/v1/admin/gates/unlock" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" \ -H "Content-Type: application/json" \ --data '{"scope":"obj:/HANDLE/f/FILE_ID/"}'

From v1.10.0, Health also reports Pending storage cleanup (quota.pending_cleanup_bytes) and how many allocations need cleanup retries (quota.cleanup_failed_allocations). Cleanup drains up to 50 allocations per sweep, so a large backlog can take several sweeps. While older sites still await conversion, a Site conversions section lists them; Upgrading to atomic site deployments explains it.

File recoveries

An interrupted loose-file write or an uncertain deletion leaves a storage reservation that blocks further changes to that file, so a late operation cannot overwrite or delete its replacement. Readers still get the file. Once a reservation needs an operator (its outcome is uncertain, its cleanup recorded an error, or its writer window passed), every PUT, PATCH, DELETE, guest write, and upload grant for that file returns 409 file_recovery_required, which clients are told not to retry. A live writer, or cleanup queued without an error, stays an ordinary retryable 409 file_busy.

From v1.11.0, while any reservation is unresolved, Health shows a File recoveries section: how many reservations are open and how many need reconciliation, then one line per reservation with the filename and file id, state and operation, whether a rollback snapshot is retained, the reservation id, when it started, and any cleanup error. GET /v1/admin/health returns the same list under file_recoveries (pending, recovery_required, and items, recovery-required first). Neither shows object keys, bytes, or secrets. A reservation from a guest write has no recorded file, so it lists File not in catalog and a null file_id.

Queued cleanup without an error finishes on its own in a later sweep. For a reservation that needs reconciliation, follow the upstream loose-file recovery procedure : read the full recovery record with a read-only query, confirm the writer has stopped, and reconcile the actual R2 objects with the catalog before releasing the charge. Do not delete the reservation row, and do not treat its age as proof that the writer stopped. Quota recompute keeps unresolved reservations charged on purpose.

Hub confirms recompute and sweep. Unlock scope is a gate_attempts key such as obj:… or ip:…. After too many bad passwords, unlock both. Responses return counts and scopes, never published bytes or secrets. Every execute is recorded in the audit log.

Revoke keys across accounts

When someone leaves, /keys only lets that person revoke their own keys. On /admin, type their handle in Keys across accounts, List, then Revoke stale or Revoke all. Type the count to confirm. The calling admin key stays live. Revoking a key also ends every upload grant it minted.

curl -fsS "$ENERGON_ORIGIN/v1/admin/tokens?owner=HANDLE" \ -H "Authorization: Bearer $ENERGON_TOKEN" PREVIEW=$(curl -fsS "$ENERGON_ORIGIN/v1/admin/tokens/revoke" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" \ -H "Content-Type: application/json" \ --data '{"owner":"HANDLE","target":"all"}') CONFIRM=$(printf '%s' "$PREVIEW" | jq -r .confirm) curl -fsS "$ENERGON_ORIGIN/v1/admin/tokens/revoke" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" \ -H "Content-Type: application/json" \ --data "{\"owner\":\"HANDLE\",\"target\":\"all\",\"confirm\":\"$CONFIRM\"}"

Listing is metadata only: owner, label, hint, scope, created, last used, expires, status. Never the secret or the hash. Drift is 409 token_revoke_drift. Missing owner is 400 bad_owner.

Retire work across accounts

POST /v1/admin/cleanup reuses the personal cleanup preview, confirm hash, skip reporting, and 100-object cap, without involvement scope. Filter by owner handle. last_read_before matches never-read objects and those whose last_read_at is older than the timestamp. Sample includes owner and last read. Last read is a floor: edge cache hits never stamp it, and hub cells say No recorded read. Loose-file reads can lag up to about a day. Do not treat a quiet last-read cell as proof nobody still opens the link.

PREVIEW=$(curl -fsS "$ENERGON_ORIGIN/v1/admin/cleanup" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" \ -H "Content-Type: application/json" \ --data '{"target":{"owner":"HANDLE","last_read_before":"2026-01-01T00:00:00.000Z"},"action":"set_ttl"}') CONFIRM=$(printf '%s' "$PREVIEW" | jq -r .confirm) # Show the human matched, eligible, bytes, and sample. Then: curl -fsS "$ENERGON_ORIGIN/v1/admin/cleanup" \ -X POST \ -H "Authorization: Bearer $ENERGON_TOKEN" \ -H "Content-Type: application/json" \ --data "{\"target\":{\"owner\":\"HANDLE\",\"last_read_before\":\"2026-01-01T00:00:00.000Z\"},\"action\":\"set_ttl\",\"confirm\":\"$CONFIRM\"}"

Admin cleanup also finds private objects from every account, including by explicit id, and reports their metadata only. Set expiry is the safe default. It defaults to 7 days so the owner sees Expires and can push it back. Expire soon is refused on anyone else’s content (400 expire_not_own) and appears on the hub only when the Owner filter is your own handle. Delete is explicit. The admin surface never returns bytes or secrets.

Offboard a person

There is no transfer-ownership feature. The handle is in the public URL and the R2 key, so a move would break saved links. Copy under the new owner at a new address, then expire the original.

  1. Remove the person from Cloudflare Access. That stops the human: no hub, no new tokens.
  2. Revoke their agent keys as above. That stops their agents immediately and ends any upload grants those tokens minted.
  3. On /admin, set Owner to their handle and Preview. You get metadata only.
  4. Keep the few things that matter with Duplicate from the hub, or duplicate_from on POST /v1/sites and POST /v1/files. Anyone with a token can already read their public work over /v1. Their private objects are readable only by their own keys, so if any must be kept, have the person export or copy them before steps 1 and 2. Afterward admins can still set their expiry or delete them, but nobody can read them.
  5. Back on the same preview, choose Set expiry (7 days unless you pick otherwise) or Delete, type the count, confirm. The 7-day grace is notice for colleagues who still hold links.
  6. Sweep now if expired objects remain, and check the storage health readout.

Do not d1 execute an UPDATE ... SET owner_id to move content. The audit log will not know about a hand edit. Use copy then expire.

Read Configure Energon policy for ADMIN_EMAILS, and Manage shared work for personal hub cleanup that does not need admin.

Last updated on