Skip to main content

Storage Plugin

What's a Storage Plugin?​

Verdaccio by default uses a file system storage plugin local-storage. The default storage can be easily replaced, either using a community plugin or creating one by your own.

API​

Storage is the one plugin type whose contract changed: it moved from callbacks to promises. Which one you implement depends on the Verdaccio you target.

VerdaccioNative contractA callback plugin
6.xcallbacksworks — it is the native one
7.xpromisesworks, wrapped by a compatibility adapter (since 7.0.0-next-7.28)

New plugins should implement the promise contract: 7.x runs it natively instead of going through the adapter, and the adapter is a compatibility shim that will not be kept forever. A callback plugin written for 6.x keeps working on 7.x, but it is the slower path and it will be the first thing to break.

Two interfaces, both from pluginUtils in @verdaccio/core. Storage handles the local database of private packages:

import { pluginUtils } from '@verdaccio/core';

interface Storage<PluginConfig> extends Plugin<PluginConfig> {
add(packageName: string): Promise<void>;
remove(packageName: string): Promise<void>;
get(): Promise<string[]>;
init(): Promise<void>;
getSecret(): Promise<string>;
setSecret(secret: string): Promise<any>;
getPackageStorage(packageName: string): StorageHandler;
search(query: searchUtils.SearchQuery): Promise<searchUtils.SearchItem[]>;
saveToken(token: Token): Promise<any>;
deleteToken(user: string, tokenKey: string): Promise<any>;
readTokens(filter: TokenFilter): Promise<Token[]>;
}

StorageHandler is returned by getPackageStorage and does the I/O for one package's manifest and tarballs:

interface StorageHandler {
logger: Logger;
createPackage(packageName: string, manifest: Manifest): Promise<void>;
readPackage(packageName: string): Promise<Manifest>;
savePackage(packageName: string, manifest: Manifest): Promise<void>;
deletePackage(fileName: string): Promise<void>;
removePackage(packageName: string): Promise<void>;
updatePackage(
packageName: string,
handleUpdate: (manifest: Manifest) => Promise<Manifest>
): Promise<Manifest>;
readTarball(fileName: string, { signal }: { signal: AbortSignal }): Promise<Readable>;
writeTarball(fileName: string, { signal }: { signal: AbortSignal }): Promise<Writable>;
hasTarball(fileName: string): Promise<boolean>;
hasPackage(packageName: string): Promise<boolean>;
}

Note that readTarball and writeTarball return real Node streams and receive an AbortSignal: a plugin is expected to stop the transfer when the client disconnects.

How Verdaccio tells them apart​

7.x decides by arity, not by configuration: a get that takes an argument and an add that takes two are read as the callback contract, and the plugin is wrapped. There is nothing to declare — but it also means a promise-based get() must take no parameters.

How the store drives your plugin​

The interfaces above do not say when Verdaccio calls what, and the tarball path in particular has a contract that is easy to get wrong. This is what packages/store/src/storage.ts actually does.

Publishing a tarball​

Three rules follow, and breaking any of them fails in a way that is hard to read:

  • You must emit open, and not before the store is listening. The store attaches the listener after awaiting writeTarball, so an open emitted synchronously — or on process.nextTick, which runs before promise continuations — is missed and the publish hangs forever with no error. Emit it from a real async source, or with setImmediate.
  • You must emit close once the bytes are durable. That is the store's signal to write the manifest. No close, no published version, even though the upload succeeded.
  • Emit error at most once. After a failure the store may have dropped its listener, and a second error on a stream with no listener is an uncaught exception that takes the whole registry down. Guard the emit with a flag.

The signal is an AbortSignal wired to the client connection. When a client disconnects mid-upload the pipeline is destroyed, and any promise your backend still has in flight must have its rejection consumed — an unhandled rejection is, again, a dead process.

Reading a tarball​

readTarball(filename, { signal }) returns a Readable that the store pipes to the response. Simpler than writing — except for one way of dying, which is worth seeing:

A missing object usually surfaces twice in object-store SDKs: once as a status code on the response, once as an error on the stream. Emitting getNotFound() for both is the most common way to crash a storage plugin. Guard the emit with a flag so only the first one gets through.

What is never called​

filterByQuery and getScore appear in several published plugins because they were copied from @verdaccio/local-storage. Nothing in the store, the API or the web calls them — the query filtering happens inside your search. They are dead weight.

Reference implementations​

Two maintained plugins are worth reading before writing your own; both back a remote object store, which is the interesting case:

PluginBackendGood for learning
verdaccio-aws-s3-storageS3resumable uploads, abort handling, the AbortSignal path
verdaccio-google-cloudGCS + Datastoresplitting bytes from registry state, and its architecture notes

Both learned the rules above the hard way, and their regression tests are the clearest statement of each one.

Two things they expose that a filesystem plugin never has to think about:

  • Object stores have no directories. removePackage cannot delete a folder: there is no object at my-package, only objects under the my-package/ prefix. Deleting the name 404s and leaves every tarball orphaned in the bucket.
  • Nothing serialises writes across processes. Verdaccio serialises concurrent writes to a package within one process. Two instances publishing different versions of the same package at the same moment are not serialised by the plugin, and the last writer wins on the manifest.

Finally, max_body_size (default 10mb) rejects larger tarballs before your plugin is ever called. Registries holding big artifacts need it raised.

Generate a storage plugin​

Run yo verdaccio-plugin and pick storage when asked for the plugin type; the plugin generator page covers installation and the prompts. The scaffold implements the promise contract against the current @verdaccio/core.