The problem
occ app:remove deletes a Nextcloud app's code and nothing else. Its tables, settings, migration records, background jobs and appdata folder stay.
Issue #5539, "Detect / Delete App Data", has been open since June 2017 and has 250 reactions. Users describe dropping tables by hand. A second use case is resetting an app whose settings break it: reinstalling does not help, because the old config survives. Polls added its own occ polls:db:purge, which covers one app while it is installed.
Nextcloud 35 added occ db:schema:check. Its verbose output lists tables it cannot attribute as "(unknown app)", and its help text says that "occ app:remove only deletes its code, not its tables or config". In October 2026 a user described that output in the issue and asked for "a program to delete tables and related data for all (unknown app) tables". I found no existing project for it.
How nc-app-purge works
nc-app-purge is a PHP script that boots Nextcloud in-process, as occ does, and uses its database connection, so SQLite, MariaDB and PostgreSQL all work.
To decide which tables belong to which app, it replays the migrations of core and of every app whose code is present into an in-memory schema and records which app created which table. This is the approach of db:schema:check; only changeSchema() runs, so nothing is written. Tables of installed apps are therefore never candidates.
A removed app has no code, so its migrations cannot be replayed. For these apps the tool downloads the code from the app store through Nextcloud's installer, which checks the app certificate and signature, replays the migrations to get the exact table list, and deletes the code again. The app is never enabled. Apps that are not in the app store can only be matched by table name, and such tables are dropped only with --by-name.
It also removes the app's rows in appconfig, preferences, migrations and notifications, its background jobs and its appdata folder. Before that it writes a backup (table definitions, rows as JSON lines, appdata copy) that nc-app-purge restore puts back. Enabled and shipped apps are refused. Purging a disabled app covers the reset case: enabling it again installs it from scratch.
Example
This is the scan from a CI run against Nextcloud 35.0.2 on PostgreSQL, after occ app:remove deck and a test app that is not in the app store:
Nextcloud 35.0.2.2 (pgsql, table prefix oc_)
APP STATUS TABLES ROWS CONFIG PREFS MIGR JOBS NOTIF APPDATA
deck removed 0+10? 30 4 2 16 4 0 26 B
ghostapp removed 0+1? 1 1 0 1 0 0 -
Without the code, Deck's ten tables only match by name (+10?). purge deck downloads Deck 1.19.2, attributes all ten tables through its migrations and then lists what it will drop and delete: ten tables, four config values, two user preferences, 16 migration records, four background jobs and the appdata folder.
Tests
The integration test runs in GitHub Actions against real Nextcloud 35 (SQLite, MariaDB, PostgreSQL), 34 (SQLite, MariaDB) and 33 (PostgreSQL). It installs Deck, creates boards through the Deck API, adds config, a preference, an appdata file and a fake app with a binary column, and runs occ app:remove. It then checks that every Deck table is attributed after the download, a dry run changes nothing, enabled apps are refused, the purge leaves nothing behind and db:schema:check stops reporting the tables, the restore brings back boards, config, appdata and binary data, and a reinstall starts empty.
Locally only the unit tests ran; every test against Nextcloud runs in CI.
Limits
Nextcloud 33 to 35 only. Exact attribution needs the app's code on disk, via --source, or from the app store for the running version; otherwise only names match. Downloading runs the app's migration classes, with the same trust as installing it. Activity entries or shares other apps keep about the removed app are not touched. Object storage and Oracle are untested. Restore needs the same database type and prefix. Take a database backup first.
The code is MIT licensed: github.com/Jwrede/nc-app-purge.