Skip to content

Deleting Extensions

Thomas Neidhart edited this page Sep 3, 2026 · 1 revision

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, version, type,
    created_timestamp, accessed_timestamp, description
) VALUES (
    nextval('personal_access_token_seq'), <user-id>, '<a-strong-random-token>', true, 0, 'LLT',
    current_timestamp, current_timestamp, 'Admin API token'
);

Three things are easy to get wrong here:

  • version and type are required — both are NOT NULL with no default (added in V1_72__Trusted_Publisher.sql). Omitting them fails with null value in column "version" ... violates not-null constraint. Use type = 'LLT' for a normal long-lived token; OTT and TPT are for one-time and trusted-publishing tokens, which the server issues itself.
  • version = 0 means "this value is still plaintext". Tokens are stored hashed. Inserting the raw token with version = 0 is correct and intended: the first request that uses it hashes it in place and bumps it to version 1. Do not insert a version = 1 row with a plaintext value — it will never match. No restart is needed for the token to start working.
  • 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.

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