Undo and the trash

Deleting an app or a resource moves it to the trash for 30 days, a reversible write records its undo, and a dict, a secret or a setting keeps what it held. Only purging is permanent.

NoClick is reversible by default. Deleting an app, database, bucket, dict, queue, repo, volume, inbox or agent doesn't destroy it: it goes to the trash, where it stays restorable for 30 days and is then purged. A write that can be taken back records how, so its operation can be undone, and a dict, a bucket's files, a secret or a setting keeps what it held before. Only what truly can't be undone (purging the trash, money leaving the account, a message reaching a person) waits for a person when an agent does it.

The trash

delete() on a trashable resource returns its TrashItem:

import noclick

item = noclick.Database.from_name("orders").delete()
print(item.kind, item.name, item.purge_after)   # database orders 2026-11-14 ...

item.restore()   # back under its name, every table and row with it

While it's in the trash:

  • it's gone from every list and read, and its name is free for something new;
  • what it holds is kept, and its storage is billed at the storage rate as before (a database's connection URLs stop working, and nothing reads or writes it; an app serves nothing, and what it keeps, its releases and build outputs, is billed as they were);
  • restoring it is free.
KindIn the trashRestoring brings back
Appoffline: its releases, deployments, settings, secrets, bindings and schedules, and the domains it was served atthe app, served again from the same release with no build, at its address and domains, its schedules firing (not while its repo is in the trash)
Databaseits tables and rows, and its branches deleted before itthe database under its name (a branch only beside its source)
Bucketits filesthe bucket and every file
Dictits keysthe dict and its keys, expiries kept
Queueits messagesthe queue and its messages
Repoits historythe repo, every branch and commit
Volumeits filesthe volume and its files
Inboxits mail; its address takes no mailthe inbox at its address, taken back from an inbox made there since once that one is deleted (while it's live, the restore is refused with its id as existing_id)
Agentits sub-agents, conversations and memorythe agent and its sub-agents; wake-ups and jobs that ended stay ended

List what's there, newest first, and put something back or purge it now:

for item in noclick.TrashItem.list(kind="bucket"):
    print(item.name, item.trashed_at, item.purge_after)

stale = noclick.TrashItem.list(kind="volume")
for item in stale:
    item.purge()   # gone for good: this one can't be undone

Restoring is refused with noclick.Conflict when something new has taken the name since: rename or delete that one first, then restore. An app's address is held for it against every other account; a new app of yours with its name can take it, and the old one's restore then says so. Purging a repo purges the apps in the trash that deploy it, which can't come back without it.

Undo an operation

A reversible write records an operation that knows how to take it back. undo() runs that once:

seen = noclick.Dict.from_name("processed-orders")
cleared = seen.clear()
cleared.undo()   # every key it removed that nothing has written since is back

item = noclick.Queue.from_name("invoices").delete()
noclick.Operation.from_id(item.operation_id).undo()   # the same as item.restore()
WriteIts undo
Deleting a trashable resourcerestores it from the trash
Clearing a dictputs back the keys it removed, leaving newer writes as they are
A database migrationrestores the database to the moment before it ran (bookmark on the result)
Restoring a SQLite database to a momentrestores it to the moment before the restore
Promoting a deploymentrestores each database the app binds to the moment before production switched, and puts the release before back in production

Every promote bookmarks the app's databases just before production switches, so a release that wrote bad data comes back out with its data:

app = noclick.Project.from_name("booking")
promoted = app.deployments(branch="new-checkout")[0].promote()
promoted.wait()
# ... it charged the wrong prices ...
promoted.undo()   # the orders table as it was, and the previous release live again

A database keeps 30 days of history on SQLite and 7 on Postgres, so a promote can be undone that long after.

An operation's undoable says whether undo() can take it back now. Undoing twice, or undoing what reached the world (an email sent, a call placed, a payment), raises noclick.Conflict. Over HTTP it's POST /v1/operations/{id}/undo.

A dict's history

Every write to a dict is logged for 30 days with the value it replaced, so a single key can be put back:

prefs = noclick.Dict.from_name("prefs")
for change in prefs.history("theme"):
    print(change.at, change.op, change.previous)   # changed, deleted or created

prefs.restore_change(prefs.history("theme")[0])   # the value it had before that write

A key changed again within a minute of its last entry adds none, so a busy counter keeps one restore point a minute.

A file's earlier versions

A bucket keeps what a file held before each write through NoClick replaced or removed it, for 30 days:

bucket = noclick.Bucket.from_name("uploads")
for version in bucket.versions("menu.pdf"):
    print(version.replaced_at, version.op, version.size)   # changed or deleted

bucket.restore_version("menu.pdf", bucket.versions("menu.pdf")[0])

Versions are kept in the bucket itself, copied in place and deleted by its own lifecycle when their days are up, so they're stored (and billed) like its files but never listed as files or served at a public URL. An upload through a signed link or S3 credentials goes straight to the store and keeps none.

Earlier values of secrets and settings

A secret keeps the values it held before each change for 30 days, sealed: its versions say when each was replaced and which keys it held, never a value, and restoring one puts the sealed values back without showing them.

stripe = noclick.Secret.from_name("stripe")
print([v.keys for v in stripe.versions()])
stripe.restore_version(stripe.versions()[0])

app = noclick.Project.from_name("booking")
app.secrets.delete("MAPS_API_KEY")
app.secrets.restore("MAPS_API_KEY", app.secrets.versions("MAPS_API_KEY")[0])
app.settings.restore("PUBLIC_BASE_URL", app.settings.versions("PUBLIC_BASE_URL")[0])

An app's secrets and settings keep their deletions too, so a deleted one comes back the same way; the app sees it at its next deployment. Deleting a noclick.Secret is for good: its versions go with it.

What waits for a person

Because deletes, migrations and config changes can be undone, an agent acting through NoClick (an MCP connection, a hosted agent's noclick tool, the account's assistant) does them without asking. The policy can still hold any of them. What can't be undone keeps waiting for a person:

  • purging the trash;
  • deleting what has no trash: a secret, a phone number, a domain, a user, a key;
  • money leaving the account, and consent to terms;
  • a message to a real person outside the account.