Skip to content

Deleting Extensions

Thomas Neidhart edited this page Sep 4, 2026 · 2 revisions

Deleting Extensions

This guide covers removing extensions from a registry — both open-vsx.org and a self-hosted private one. Deleting is irreversible, so read Delete vs. purge first.

Delete vs. purge

These are two different operations and the difference matters:

what happens can the version be published again?
delete the version's files are removed, and its identity stays reserved no, never
purge the version is physically removed from the database and storage yes

Extension versions are otherwise immutable, so delete deliberately keeps the version number claimed: nobody — including the original publisher — can later publish different content under a version number somebody already installed. If you deleted a version by mistake and genuinely need that number back, you need purge, which is admin-only.

Who may delete what

Deleting through the regular API acts on behalf of the user owning the personal access token:

  • namespace owners may delete any version in the namespace;
  • other namespace members may delete only the versions they published themselves;
  • anyone else may not delete anything.

Deleting an extension you don't own, or purging anything, requires a user with the admin role and the admin API.

Finding what to delete

The CLI can't enumerate a registry - ovsx get --metadata needs an identifier you already have - so use the API when you don't know what's published. Both endpoints are public, no token needed.

List the extensions in a namespace:

curl -s "https://openvsx.example.com/api/my-namespace" | jq '.extensions'

List the versions of one extension:

curl -s "https://openvsx.example.com/api/my-namespace/my-extension/versions" | jq '.versions'

To search across namespaces, use /api/-/search?query=... (see the OpenAPI docs at /swagger-ui on your registry for its parameters).

Using the web UI

If you can log in, this is the shortest route: open your extension's page, and each version you are allowed to delete offers the action there. Nothing else to install or configure.

The rest of this guide is for automating it, or for registries where nobody can log in.

Using the CLI

ovsx unpublish needs a registry running 1.2.0 or later; it checks the registry version up front and tells you if it's too old.

You need a personal access token; see Publishing extensions if you don't have one. Set the registry and token once, or pass --registryUrl / --pat on each call:

export OVSX_REGISTRY_URL=https://openvsx.example.com
export OVSX_PAT=<your-personal-access-token>

Delete an extension with all of its versions:

ovsx unpublish my-namespace.my-extension

Delete specific versions:

ovsx unpublish my-namespace.my-extension --versions 1.2.3 1.2.4

Delete only certain target platforms of a version. --target requires --versions:

ovsx unpublish my-namespace.my-extension --versions 1.3.0 --target linux-x64 win32-x64

Run from an extension folder and the identifier is read from package.json:

ovsx unpublish

Each of these asks for confirmation first. In CI — or any non-interactive shell — it refuses rather than prompting, so pass --force when you mean it:

ovsx unpublish my-namespace.my-extension --force

Using the API directly

Useful when you'd rather not install Node, e.g. in a restricted environment. This is exactly what the CLI calls.

Delete the whole extension — note the allVersions parameter, without which nothing is deleted:

curl -X POST \
  "https://openvsx.example.com/api/my-namespace/my-extension/delete?token=$OVSX_PAT&allVersions=true"

Delete specific versions by posting a list. An entry without targetPlatform deletes every target platform of that version:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '[{"version": "1.2.3"}, {"version": "1.2.4"}]' \
  "https://openvsx.example.com/api/my-namespace/my-extension/delete?token=$OVSX_PAT"

Restrict a version to one target platform:

curl -X POST \
  -H "Content-Type: application/json" \
  -d '[{"version": "1.3.0", "targetPlatform": "linux-x64"}]' \
  "https://openvsx.example.com/api/my-namespace/my-extension/delete?token=$OVSX_PAT"

The response reports one line per deleted version, and nothing at all if there was nothing to delete — so an empty success means your selection matched no versions, not that it deleted everything.

Using the admin API

For deleting extensions you don't own, and for purging. Both need a token belonging to a user with the admin role. The request bodies are the same as above.

# delete (identity stays reserved)
curl -X POST \
  -H "Content-Type: application/json" \
  -d '[{"version": "1.2.3"}]' \
  "https://openvsx.example.com/admin/api/extension/my-namespace/my-extension/delete?token=$ADMIN_PAT"

# purge (frees the version for republishing)
curl -X POST \
  -H "Content-Type: application/json" \
  -d '[{"version": "1.2.3"}]' \
  "https://openvsx.example.com/admin/api/extension/my-namespace/my-extension/purge?token=$ADMIN_PAT"

An admin can do the same from the web UI, under Admin Dashboard → Extensions (/admin-dashboard/extensions), which offers both delete and purge per version.

Registries without a login provider

A private registry deployed without any OAuth2 provider — an air-gapped cluster, say — has no way to log a user in, so there is no user to own a token and nothing can be deleted through any of the routes above. You can create the first user and its token directly in the database.

Connect to the registry's PostgreSQL database, then:

-- An admin user. Drop the role to create an ordinary publisher instead.
INSERT INTO user_data (id, login_name, role)
VALUES (nextval('user_data_seq'), 'openvsx-admin', 'admin')
RETURNING id;

-- A long-lived token for that user. Substitute the id returned above for <user-id>,
-- and a strong random string for the token value.
INSERT INTO personal_access_token (
    id, user_data, value, active,
    created_timestamp, accessed_timestamp, description
) VALUES (
    nextval('personal_access_token_seq'), <user-id>, '<a-strong-random-token>', true,
    current_timestamp, current_timestamp, 'Admin API token'
);

Three things are worth knowing here:

  • Insert the token in plaintext; the registry hashes it on first use. Stored tokens are hashed, but the hash is peppered from the application's own configuration, so there is no way to compute one by hand. The version column records which format value is in and defaults to 0, meaning "still plaintext": the first request that uses the token hashes it in place and bumps it to version 1, and a job at startup does the same for tokens that are never used. No restart is needed for the token to start working. Don't insert a version = 1 row with a plaintext value — it will never match anything.
  • The token's type defaults to LLT, a normal long-lived token, which is what you want here. OTT and TPT are the one-time and trusted-publishing types, which the server issues itself as part of flows this INSERT isn't part of.
  • Use the sequences rather than hardcoding ids. user_data_seq and personal_access_token_seq are what the application allocates from, so a hardcoded id can collide with a row the registry creates later.

On a registry whose schema is between V1_72__Trusted_Publisher.sql (which added version and type as NOT NULL) and V1_74__PersonalAccessToken_Column_Defaults.sql (which gave them the defaults above), the INSERT fails with null value in column "version" ... violates not-null constraint. Name the two columns explicitly there — add version, type to the column list and 0, 'LLT' to the values — which also works against every newer schema.

The role column accepts admin or privileged, and these are not interchangeable:

  • admin grants the admin API and dashboard — deleting, purging, managing users and namespaces.
  • privileged allows publishing to any namespace, bypassing ownership checks. It grants no admin access, so it cannot delete other people's extensions.

Leave role unset for an ordinary user, which is enough to publish to and delete from its own namespaces.

Note that for local development you don't need any of this: the dev Gradle source set seeds a super_user with the admin role and the token super_token (server/src/dev/resources/db/migration). See doc/development.md.

Deleting directly in the database

Don't, unless you have no alternative. Extension data spans extension, extension_version, file_resource and several dependent tables, storage holds files the database rows point at, and the search index is updated by the application. Deleting rows by hand bypasses all of that and leaves orphaned files and a stale index behind.

If you must, back the database up first. Note that this is also the one route that removes a version's reservation without a purge, which means content can later be republished under a version number that clients may already have cached.

Clone this wiki locally