A self-hostable WebAssembly package registry built for Cloudflare Workers, with D1 for accounts and metadata and private R2 storage for artifacts. Register with email and password or GitHub, claim a namespace, and publish immutable modules and components.
- Features
- Deploy to Cloudflare Workers
- Deploy a GitHub fork with Workers Builds
- Update your fork from upstream
- Local development and manual deployment
- Configuration
- Authentication
- Publish and download packages
- Storage, consistency, and backups
- Monitoring and maintenance
- Limits
- Architecture and interface
- Development and verification
- Project references
- Email/password and GitHub registration and login, Argon2id password hashing, HttpOnly sessions, CSRF protection, authentication throttling, and revocable tokens.
- Namespace ownership enforced for browser sessions and personal tokens.
- SHA-256 content-addressed artifacts and immutable semantic versions.
- Search, package/release pages, compatible-version resolution, and yank/unyank.
- Component interfaces, reconstructed WIT, worlds, imports, exports, and dependency graphs.
- Browser publishing, Wasmd CLI compatibility, and the standalone
wrclient. - English multi-page UI and an embedded OpenAPI reference at
/openapi.yaml.
Choose Fork + Workers Builds if you want GitHub's Sync fork button for future updates.
Use the Deploy to Cloudflare button above to create your own repository and deployment.
You need a Workers Paid account, R2 enabled, and a GitHub OAuth App for GitHub
sign-in. Cloudflare creates a repository, provisions the resources in
wrangler.jsonc, and configures Workers Builds. Complete these settings on the
Set up your application page before selecting Deploy:
| Setup field | What to enter or select |
|---|---|
| Git account / Project name | Choose the destination Git account and the new Worker name. |
Select D1 database (DB) |
Select an existing Registry database, or Create new and enter a database name. |
Select R2 bucket (BLOBS) |
Select the matching existing Registry bucket, or Create new and enter a bucket name. |
GITHUB_CLIENT_ID |
Paste the Client ID from your GitHub OAuth App. |
GITHUB_CLIENT_SECRET |
Paste the client secret from that same OAuth App. |
The two GitHub secret fields are declared in .env.example, using
the same template mechanism as Payload's Cloudflare D1 template.
Cloudflare reads the declaration and the field descriptions in package.json to
collect the values and configure Worker secrets during setup. The example contains
empty values; never replace them with real credentials in Git. No post-deployment
wrangler secret put commands are needed when both values are supplied here.
Before deploying, create the GitHub OAuth App with
the final Worker origin as its Homepage URL and that origin plus
/v1/auth/github/callback as its callback URL. For example, Worker name registry
and account Workers subdomain onebitbank produce
https://registry.onebitbank.workers.dev/v1/auth/github/callback. If you change
the Worker name or use a custom domain, update the GitHub App URLs accordingly.
Cloudflare collects existing OAuth credentials; it does not create the GitHub
OAuth App for you.
When reusing storage, select an empty database or one already used by this registry, and pair existing Registry metadata with its original artifact bucket. Pending migrations run against the selected database; they do not import another app's schema. New resources are created and bound automatically. Keep R2 private.
Use the Deploy to Cloudflare button to enter this template flow; a generic
Import a repository flow may not show these template fields. If Cloudflare
reports Repository not found, ensure the source repository is public and the
template changes, including .env.example, have been pushed to the branch used
by the button. Local changes cannot affect the hosted setup form.
Check the commands before deploying;
Cloudflare may default to npx wrangler deploy, which bypasses D1 migrations.
Set these commands explicitly:
| Setting | Value |
|---|---|
| Root directory | Repository root |
| Build command | npm run build |
| Deploy command | npm run deploy |
The source template intentionally omits database_id. npm run deploy builds
the application and runs tools/deploy-worker.mjs in this order:
- If the one-click setup supplied a real
DB.database_id, use it directly. Otherwise, look updatabase_namein the current Cloudflare account and create that D1 database if it does not exist. A later build reuses the same database. - Write a temporary deployment configuration containing the actual UUID, then
run
wrangler d1 migrations apply DB --remotewith that configuration. Migrations create the application tables and record completed migrations ind1_migrations. - Check that every application table exists, then deploy the Worker using the same configuration and Wrangler's automatic resource provisioning for R2. Existing buckets are reused.
Database or migration failure stops publication. Database names customized in the
one-click setup are honored; migrations always reference the DB binding. The
temporary file is removed after the run and the source template is not overwritten
with a particular account's UUID. Do not put an all-zero UUID into the configuration:
Wrangler treats a nonempty ID as a configured resource, not a request to create one.
After deployment, open the generated HTTPS URL and register an account. There is no first-user administrator elevation: each account can claim unowned namespaces, and only the owner can publish or yank within that namespace. Create a personal token under Your account to use the existing CLI.
Fork the repository, connect it to Cloudflare, and use npm run deploy as
Workers Builds' deploy command. It provisions or reuses D1/R2, applies pending D1
migrations, and publishes the Worker. After deployment, configure GitHub login
secrets and verify runtime variables and storage bindings in the dashboard.
Future code updates use GitHub's Sync fork button.
You can complete this flow in the browser. Workers Builds installs dependencies
and builds the Rust/Wasm helper; no local tools or GitHub Actions workflow are
required. Use a Cloudflare account with Workers Paid (required by the committed
cpu_ms: 30000 setting) and R2 enabled.
The walkthrough follows the organization of the
OpenList deployment guide,
with Registry-specific configuration. The GitHub image is a real screenshot of
liusha-com/registry; the other figures are labeled configuration diagrams.
Dashboard labels may vary with language and Cloudflare UI updates.
- Sign in to GitHub and open liusha-com/registry.
- Click Fork, select your account or organization, and keep the repository
name
registry. Select Copy the main branch only if offered. - Click Create fork. Your repository should display forked from liusha-com/registry below its name.
YOUR_ACCOUNT/registry below means your fork. Cloudflare must connect to it.
Creating an empty repository, importing a copy, or using a template does not
establish the fork relationship needed by Sync fork.
A fork of this public repository is public too. D1/R2 data and private Registry
namespaces remain private. Store credentials in Cloudflare secrets, never in Git.
A private template-created repository such as fengmao/registry-prod does not
become a fork by adding an upstream Git remote. See
GitHub's fork visibility rules.
- Open Cloudflare Workers & Pages in the target account and select Create application.
- Choose Import a repository → Get started, or Continue with GitHub / Connect to GitHub, depending on the dashboard version. Select a Worker.
- Authorize Cloudflare's GitHub integration to access your fork, then select
YOUR_ACCOUNT/registry. If it is missing, update the Cloudflare GitHub App's repository access. Organizations may require owner approval. - Enter the following settings, then select Save and Deploy / Deploy.
| Setting | Value |
|---|---|
| Worker/project name | registry, matching name in wrangler.jsonc |
| Repository | YOUR_ACCOUNT/registry |
| Production branch | main |
| Root directory | Repository root (/ in the dashboard) |
| Build command | npm run build |
| Deploy command | npm run deploy |
| Build environment variable | NODE_VERSION = 22 |
| Builds for non-production branches | Disabled for this production setup |
Keep automatic dependency installation enabled. The npm lockfile pins the tools, and the build script installs Rustup and its target when necessary. The deploy command also invokes the build, reusing matching Wasm output within the build.
Use npm run deploy for every build in this guide. The command reads
wrangler.jsonc, resolves or creates D1, applies pending migrations, checks the
schema, then deploys with R2 provisioning. A migration failure stops publication.
Do not replace it with npx wrangler deploy, which skips the migration wrapper.
The Git import flow does not collect the one-click template's resource selectors
or OAuth secrets. With an unchanged fork, the deployment uses D1
wasmd-registry and R2 wasmd-registry-blobs. To use different resources, set
database_name (or the real database_id) and bucket_name in your fork's
wrangler.jsonc before the first build. Pair an existing Registry database
with its original artifact bucket. Independent instances need distinct storage.
For an existing Worker, connect the fork under Settings → Builds → Connect
and set the same commands there. Match its name, D1/R2 resources, and plaintext
settings in wrangler.jsonc before building. Keep credentials in Worker secrets.
Review automatic configuration PRs before merging them. See
Cloudflare's Git integration guide.
After deployment, open Workers & Pages → registry → Settings → Variables and
Secrets (sometimes Runtime variables and secrets). Verify the following
Text values, which are supplied by vars in wrangler.jsonc. Add or adjust
values here if needed; enter them without surrounding quotes. For changes to
survive future builds, also update vars in your fork's wrangler.jsonc.
Dashboard-only plaintext changes can be overwritten by the next deployment.
| Type | Name | Initial value |
|---|---|---|
| Text | PUBLIC_READ |
true |
| Text | REGISTRATION_ENABLED |
true |
| Text | MAX_BLOB_BYTES |
8388608 |
| Text | MAX_ANALYSIS_BYTES |
8388608 |
These enable anonymous access to public packages and registration, and set the artifact upload/analysis limits to 8 MiB. See Configuration for details. Private namespaces still require their owner's authentication.
To enable Continue with GitHub, create a GitHub OAuth App and add the following entries using type Secret on the same Worker page:
| Type | Name | Value |
|---|---|---|
| Secret | GITHUB_CLIENT_ID |
Your GitHub OAuth App's Client ID |
| Secret | GITHUB_CLIENT_SECRET |
The same OAuth App's generated client secret |
Set the OAuth App's Homepage URL to your deployed origin and its
Authorization callback URL to that origin plus /v1/auth/github/callback.
For example: https://registry.YOUR_SUBDOMAIN.workers.dev/v1/auth/github/callback.
Save and Deploy the settings when prompted. Email/password authentication
works without the two GitHub secrets once storage is initialized.
Add these values to the Worker runtime, not Settings → Builds → Variables
and secrets. Build settings are only available while compiling/deploying.
The GitHub connection for source access is separate from the OAuth App for user
login. .env.example documents secret names; it does not set production values.
See Cloudflare runtime variables
and runtime secrets.
A successful npm run deploy normally creates or reuses and binds these
resources automatically. Verify them under Worker → Bindings. If you need to
create resources yourself or correct a binding, use the dashboard steps below
and set the same resource names/IDs in wrangler.jsonc before redeploying.
The binding names must be exactly DB and BLOBS, including capitalization.
Dashboard-only binding changes can be replaced by the next build's configuration.
| Resource | Suggested resource name | Worker binding |
|---|---|---|
| D1 database | wasmd-registry |
DB |
| Private R2 bucket | wasmd-registry-blobs |
BLOBS |
D1 database:
- In the Cloudflare account, open Storage & databases → D1 SQL Database.
- Select Create database, enter
wasmd-registry(or another name), and finish creation. To reuse storage, choose an existing database belonging to Registry. - Return to Workers & Pages → registry → Bindings → Add binding.
- Select D1 database, set the variable/binding name to
DB, select that database, and click Add binding. Save/deploy changes if prompted.
R2 bucket:
- Open Storage & databases → R2 Object Storage and enable R2 if required.
- Select Create bucket, enter
wasmd-registry-blobs(or another name), and create the bucket. You may instead select an existing Registry bucket. - Return to the Worker's Bindings → Add binding, select R2 bucket, set
the variable/binding name to
BLOBS, and choose the bucket. - Save/deploy the binding. Keep the bucket private; Registry serves authorized downloads through the Worker. No public bucket URL is needed.
When reusing a Registry database, select its original artifact bucket too. Each
independent Registry instance needs its own storage pair. Adding a binding alone
creates no application tables. After changing storage, update wrangler.jsonc
and retry the build with npm run deploy to apply migrations to the selected
D1 database. See Cloudflare's
D1 dashboard setup and
R2 bucket creation guide.
In Worker → Deployments / Builds, check that the deployment log shows
d1 migrations apply and schema checks before Worker publication. The command
runs SQL files from worker/migrations in order and records completed files in
d1_migrations. Later builds apply only pending files.
Open D1 SQL Database → the database bound as DB → Console and verify:
SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name;
SELECT name, applied_at FROM d1_migrations ORDER BY id;
SELECT email FROM users LIMIT 0;
SELECT visibility FROM namespaces LIMIT 0;
SELECT overview, overview_revision FROM packages LIMIT 0;You should see tables including users, namespaces, packages, versions,
and d1_migrations; the column checks should succeed even before users register.
If tables are missing, verify that wrangler.jsonc selects the same D1 database
as the Worker's DB binding, then retry the build using npm run deploy and
inspect the migration log. If migration history and the existing schema disagree,
inspect the failed SQL before retrying; do not blindly replay completed
ALTER TABLE statements or mark failed migrations as complete. See
repairing an empty D1 database.
- In the Worker dashboard, verify the
DBandBLOBSbindings, four runtime text variables, and both GitHub secrets if GitHub login is enabled. - Visit
https://YOUR_WORKER_HOST/readyz; it should return a successful response after checking D1 and R2. - Open
/register, register with email/password, create a namespace, publish a small Wasm file, and download it to verify the complete storage path. - For GitHub login, check
/v1/auth/providersreturns"github": true, then test Continue with GitHub. A callback error usually means the OAuth App URLs do not match the address in your browser.
Optionally add a domain under Settings → Domains & Routes → Custom Domain. After it is active, update both OAuth App URLs to that origin. See Workers Custom Domains.
Workers Builds runs npm run deploy after your fork's production branch
changes. Plaintext variables and D1/R2 bindings are deployed from
wrangler.jsonc; keep that file aligned with the intended production settings.
Runtime GitHub secrets stay on the same Worker across normal deployments.
Accounts, namespaces, overviews, and artifacts live in D1/R2, outside Git.
- Review changes in liusha-com/registry, including new migrations and runtime settings. Back up existing data before a production update and follow any release-specific migration instructions.
- Open your fork → main → Sync fork → Update branch. If GitHub reports
conflicts, resolve them in the offered PR and preserve your Worker name,
resource selections, and custom
varsinwrangler.jsonc. - Cloudflare builds the updated
mainand runsnpm run deploy. Pending D1 migrations run before publication. Watch Worker → Deployments / Builds and verify the deployed commit matches the updated fork. - Add any newly required runtime secrets in the Worker dashboard. Verify the
deployed variables and
DB/BLOBSbindings, then check/readyz, login, and an existing package download.
If GitHub says the branch is up to date, there is nothing to sync. See GitHub's Sync fork guide. Fork-specific source/configuration edits can still cause merge conflicts.
Using the same Worker and configured resources does not clear storage. Changing a binding may point the site at different data without deleting the old resource. A code rollback does not undo SQL migrations.
Moving from a template-created repository: create a real GitHub fork, record
the existing Worker's name, plaintext variables, D1 name/ID, and R2 bucket, and
set those values in the fork's wrangler.jsonc before connecting the existing
Worker to it. Use npm run deploy as the deploy command and keep the existing
Worker secrets. Reconnecting Git does not move data. A public fork cannot replace
a private repository's visibility requirements.
| Symptom | Check |
|---|---|
| Fork missing from repository picker | Grant the Cloudflare GitHub App access to the fork and select the correct account/organization. |
| Worker name mismatch | Dashboard Worker name and wrangler.jsonc name must match. |
| Build fails | Confirm the deploy command is npm run deploy and inspect the first failing step in the build log. |
| Registration reports storage unavailable | Verify DB and BLOBS match wrangler.jsonc, and check migration logs. |
| D1 exists but has no tables | Retry the build with npm run deploy; check that migrations target the same database as the Worker's DB binding. |
| R2 missing from binding picker | Enable R2 and create/select a bucket in the same Cloudflare account. |
| ENV or bindings change after an update | Update vars, D1 selection, and R2 selection in wrangler.jsonc; dashboard-only changes are not durable across builds. |
| GitHub sign-in is missing | Add both OAuth values as Worker runtime secrets, save/deploy, and check /v1/auth/providers. Build secrets alone do not enable login. |
| Duplicate column during migration | Inspect the schema and migration history before retrying; the SQL may already have run. |
| Sync fork is missing | Verify forked from liusha-com/registry appears on the repository. Template copies are independent repositories. |
| Sync succeeds but site is unchanged | Check production branch main, automatic builds, watch paths, build errors, and deployed commit. |
Install Node.js 22+ and Rustup. npm ci installs the pinned Wrangler, Wasm build
tool and test runtime. The build script uses Rust 1.98.0 and installs its
wasm32-unknown-unknown target. On Linux/macOS it can bootstrap Rustup from the
official installer; Windows users should install Rustup first. To use an already
installed compatible toolchain, set REGISTRY_RUST_TOOLCHAIN (for example, stable).
npm ci
npm run devThis builds the Wasm helper, applies local migrations and starts Wrangler on
http://localhost:8787. Local persistence is under .wrangler/state, separate
from all remote data. No Cloudflare login is needed for local development or tests.
For a manual remote deployment:
npx wrangler login
npm run deployThe deploy command creates missing resources automatically. To use an existing
database, set its real database_id in wrangler.jsonc, or set database_name
to its exact name. Use bucket_name to select an existing R2 bucket. Choose distinct
resource names when hosting independent registries in one Cloudflare account.
The same command applies new migrations on later deployments. A migration failure
stops deployment. Do not use wrangler versions upload against production bindings
for untrusted pull requests; use separate preview resources for integration tests.
If registration reports Registry storage or processing is unavailable. and D1
has no application tables, creating or binding the database alone is insufficient.
Verify that wrangler.jsonc selects the database bound to the Worker, set the
Workers Builds deploy command to npm run deploy, and retry the production
build. The log must show d1 migrations apply before Worker publication.
To apply migrations from an authenticated local checkout without building or
publishing the Worker, verify that database_name (or database_id) selects the
deployed database and run:
npx wrangler login
node tools/deploy-worker.mjs --migrate-onlyThis uses the same database resolution and migration checks as deployment.
It preserves existing data and records applied migrations in d1_migrations.
Afterwards D1 should contain the application tables (including OAuth tables) and d1_migrations;
/readyz should return success after checking both D1 and R2.
See email and GitHub authentication for OAuth App setup, required secrets, callback URLs and compatibility with existing accounts.
Edit vars in wrangler.jsonc and redeploy with npm run deploy. Boolean
settings are strings. Dashboard-only changes to plaintext variables may be
overwritten on the next build. Store GitHub OAuth credentials as Worker secrets;
normal code deployments preserve those secrets.
| Variable | Default | Meaning |
|---|---|---|
PUBLIC_READ |
"true" |
Anonymous registry API and artifact reads. Set "false" to require a session or personal token. Public HTML/assets remain available for signing in. |
REGISTRATION_ENABLED |
"true" |
Set "false" after creating accounts to close registration. |
MAX_BLOB_BYTES |
"8388608" |
Upload limit, at most 8 MiB in this profile. Enforced while reading even without Content-Length. |
MAX_ANALYSIS_BYTES |
"8388608" |
Automatic WIT inspection limit, at most 8 MiB. Larger files can still be published and downloaded within the upload limit. |
The request URL determines the allowed browser origin and secure-cookie policy, so custom domains need no hard-coded public URL. Sessions last 24 hours; personal tokens default to 90 days and can have a custom duration or no expiration. Only hashes of credentials are stored. Sessions are cookie-only, personal tokens are bearer-only, and session mutations require a CSRF token. Passwords use Argon2id (19 MiB memory, two iterations, one lane). D1 persists per-user and per-IP authentication throttling. An hourly scheduled handler removes expired credentials and throttle records.
The release page displays Component interfaces, including imports, exports,
and reconstructed WIT. Older deployments only analyzed files up to 1 MiB. With
MAX_ANALYSIS_BYTES=8388608, existing components missing metadata are analyzed
from R2 on their first /component request and cached in D1, so existing releases
do not need to be republished. Core modules and undecodable components have no
inspectable WIT; files above a customized analysis limit report that limit explicitly.
All R2 access goes through the Worker. Keep the bucket private: do not enable
r2.dev or attach a public bucket domain. Artifact downloads use attachment and
sandbox headers regardless of uploaded media type. Switching an existing public
registry to private does not revoke copies or responses previously cached by
clients; use a new private deployment if that is required.
Registration requires an email address and a password of at least 12 characters,
with a maximum of 128 UTF-8 bytes. Email addresses are trimmed and lowercased.
The registration and login APIs accept email and password; supplying username
returns HTTP 400. There is no username login fallback.
The database retains username as a stable internal identifier for sessions,
tokens, and namespace ownership. New email accounts receive a generated identifier
that does not expose the email address as the namespace owner. GitHub accounts use
GitHub's numeric user ID, so renaming a GitHub account does not create a different
registry account. Accounts are never automatically merged by matching names or
email addresses. GitHub-only accounts sign in through GitHub.
Email verification, email codes, self-service password recovery, MFA, organization teams, and account deletion are not implemented. Establish an operator-assisted account recovery process before opening registration to the public.
Upgrading existing accounts: accounts with an email can use it with their existing password. Legacy accounts without an email can no longer sign in with a password. An operator must verify ownership and attach a unique email to the original user record, preserving its internal identifier and namespace ownership. Do not delete users or recreate the database to migrate accounts. Existing valid sessions and CLI tokens continue to use the internal identifier.
Open GitHub OAuth Apps and select New OAuth App. Create an OAuth App, not a GitHub App.
| GitHub field | Example production value | Notes |
|---|---|---|
| Application name | Wasmd Registry |
Displayed during authorization; customize as needed. |
| Homepage URL | https://registry.onebitbank.workers.dev |
Replace with your registry's public origin. |
| Application description | Sign in to Wasmd Registry |
Optional. |
| Authorization callback URL | https://registry.onebitbank.workers.dev/v1/auth/github/callback |
Use your actual domain and this complete path. |
Select Register application, copy the Client ID, then select Generate a new client secret and save the generated secret.
For a custom domain, update both URLs and start sign-in from that same domain.
The callback URL is generated from the sign-in request origin plus
/v1/auth/github/callback and stored with the OAuth state. It must match the
OAuth App configuration. No GITHUB_REDIRECT_URI, GITHUB_CALLBACK_URL, or other
OAuth URL environment variable is needed.
| Parameter | Required for GitHub | Source | Storage |
|---|---|---|---|
GITHUB_CLIENT_ID |
Yes | OAuth App's Client ID | Worker runtime variable or Secret; Secret recommended. |
GITHUB_CLIENT_SECRET |
Yes | Generated OAuth App client secret | Worker runtime Secret. |
Names are case-sensitive. In the Cloudflare dashboard, open Workers & Pages → your Worker → Settings → Runtime variables and secrets to change credentials on an existing deployment, or configure them for a manual deployment. Add both values as Secret, then save and deploy the settings. These are runtime values, not Workers Builds build variables. Email/password authentication works without them. For one-click deployment, enter both secrets on the initial setup page instead.
Alternatively, run these commands from an authenticated checkout. Each secret command prompts for its value:
npx wrangler secret put GITHUB_CLIENT_ID
npx wrangler secret put GITHUB_CLIENT_SECRET
npm run deployKeep the client secret out of wrangler.jsonc, frontend code, documentation, and
Git history. It is not a user password or a registry personal access token.
With REGISTRATION_ENABLED="true", a first GitHub sign-in creates an account.
With "false", existing GitHub accounts can sign in, but new accounts cannot register.
Create a separate OAuth App for local development:
- Homepage URL:
http://localhost:8787 - Authorization callback URL:
http://localhost:8787/v1/auth/github/callback
Keep the hostname consistent: localhost and 127.0.0.1 are different origins.
Create a Git-ignored .dev.vars file in the repository root:
GITHUB_CLIENT_ID=your_local_oauth_app_client_id
GITHUB_CLIENT_SECRET=your_local_oauth_app_client_secretYou can copy .env.example to .dev.vars as a starting point. Wrangler reads
local values from .dev.vars; .env.example declares the hosted setup fields.
Run npm run dev and visit http://localhost:8787/login. Local secrets are not
uploaded to production; configure production runtime secrets separately.
- Deploy with
npm run deploy, which applies0002_email_github.sqland creates the OAuth tables along with any other pending migrations. - Open
/v1/auth/providers; complete configuration returns"github": true. - Reload
/loginor/registerand select Continue with GitHub. - Authorize the app, return to
/account, and create a namespace or personal token.
| Symptom | Check |
|---|---|
| GitHub button is missing | Both runtime parameters are present and the settings have been deployed. |
| Callback URL mismatch | Scheme, hostname, port, and full callback path match the OAuth App. |
| Sign-in request expired | Start again instead of reusing a callback URL; allow this site's cookies. |
| GitHub is temporarily unavailable | Client ID and secret belong to the same app, the secret is valid, and GitHub is reachable. |
| First sign-in cannot register | REGISTRATION_ENABLED is "true". |
| Missing database tables | The deploy command is npm run deploy and migrations completed successfully. |
OAuth requests only read:user, with no repository permissions. It uses PKCE S256,
a browser-bound HttpOnly temporary cookie, and single-use state that expires after
ten minutes. GitHub access tokens are not persisted in the database.
See GitHub's OAuth web application flow.
- Open
/registerand register with email/password or GitHub. Registration signs you in; use/loginfor subsequent visits. - Open Your account at
/accountand claim an available namespace, such asyour-team. There is no automatic first-user administrator role. - Open
/publish, enter the namespace, package name, and semantic version, and choose a.wasmartifact within the configured upload limit. - Publish the release. SHA-256 digests are verified and version coordinates are immutable; publish a new version to change the artifact.
- Browse
/explore, open the package and release pages, and download artifacts or inspect available component interfaces and WIT.
Owners can yank and unyank releases. Yanking excludes a release from normal version resolution; exact-version downloads remain available.
The account page lists Your namespaces, with package counts and each namespace's packages, descriptions, and update dates. Select a package to open its details. Empty namespaces remain visible with a publishing link. Namespace and package lists load 20 items at a time; use Load more to continue and Refresh to reload after publishing from another browser tab or the CLI. Creating a namespace on the account page refreshes the list automatically. These account lists only include namespaces owned by the signed-in user, including when public reads are enabled.
The package root (/packages/<namespace>/<package>) has Overview and
Versions tabs. The sidebar shows the latest active release, artifact type,
size, license, manifest digest, and a wasmd pull command. Each version links to
its immutable manifest, downloadable Wasm artifacts, and component interfaces.
- Browser uploads: enter Markdown in the optional Overview editor on
/publish. Use Write / Preview to check headings, code blocks, lists, tables, links, and images before publishing. A blank field preserves existing documentation. - CLI uploads: a Wasmd CLI build with overview support automatically reads
README.md(alsoreadme.mdorReadme.md) at the Git repository root containing the.wasmfile. It locates the repository from the artifact path, regardless of the current working directory, and supports Git worktrees. Outside a Git repository it checks the artifact's own directory. No README means the existing overview is preserved. Add--readme ./PACKAGE.mdtowasmd pushto select another file, or--no-readmeto keep the current overview. Older CLI builds still publish artifacts but cannot automatically send local README content; upgrade/rebuild the CLI to use this feature. - Editing later: sign in as the namespace owner, open the package root, and select Edit overview. Save or clear the content without publishing another version. If another editor or publication updates it meanwhile, saving returns a conflict instead of overwriting those changes. Copy your draft before reloading to reconcile a conflict.
Overview text is limited to 64 KiB of UTF-8. The CLI rejects oversized or non-UTF-8 README files before uploading the artifact. Rendered Markdown is sanitized; scripts, embedded forms, styles and unsafe URLs are removed. Use absolute URLs for documentation links and HTTPS image URLs; relative repository paths are not resolved. External images are loaded without a referrer.
The Worker accepts an optional top-level overview string in the publication
manifest. When present, a successful publication replaces the package overview
atomically with the release; duplicate or invalid releases leave it unchanged.
Omitting the field preserves existing text. Historical manifests and their
digests remain immutable even after the package overview is edited.
GET /v1/packages/<namespace>/<package> returns overview, overview_revision,
visibility, owner_username, and latest_release alongside package metadata
and versions. Owners can PUT /v1/packages/<namespace>/<package>/overview with
{"overview":"# Documentation","revision":1}, using the current revision from
GET. Browser sessions require CSRF protection; personal tokens use bearer auth.
Private namespace access rules apply to all overview reads and writes.
Deployment automatically applies migration 0004_package_overview.sql; existing
packages begin with an empty overview.
Choose Public or Private when creating a namespace. The API accepts
{"name":"my-team","visibility":"private"} at POST /v1/namespaces.
Omitting visibility defaults to public for existing clients.
- Public: anyone can browse its packages, releases, and published artifacts
with the default
PUBLIC_READ="true"configuration. - Private: only its owner, using a browser session or personal access token, can read or publish its packages. Other accounts have no access. Search and statistics exclude private content belonging to other users. Inaccessible namespace, package, version, artifact, and WIT requests return 404.
The registry-wide PUBLIC_READ="false" setting remains an additional sign-in
requirement for all reads; it does not grant access to another user's private
namespaces. Leave it at "true" to allow public namespaces to be read by everyone.
Visibility is chosen at creation; this version does not provide visibility changes.
Deployment applies 0003_namespace_visibility.sql. Existing namespaces remain
public, and existing release manifests are indexed to preserve artifact access.
Uploads are initially accessible only to their uploader until referenced by an
accessible release. Legacy uploads without a release or uploader record must be
uploaded again by their owner before use. Private downloads are never publicly cached.
Artifacts are content-addressed: if identical bytes are deliberately published in a public release, that digest is public even if a private release also references it. Private namespace and release metadata remain protected. Knowing a private digest alone does not grant permission to download it or publish it elsewhere.
Create a Personal access token on /account and copy it immediately; it is
shown only once. Supply it from your secret manager as WASMD_REGISTRY_TOKEN.
Replace the URL with your deployed Worker and use a namespace you have not already
claimed in the browser. The Wasmd CLI is a client of the Worker-hosted registry.
$env:WASMD_REGISTRY_TOKEN | wasmd login https://registry.example.com --token-stdin
wasmd namespace create your-team --description "Our components"
wasmd push .\hello.wasm your-team/hello:1.0.0
wasmd pull your-team/hello:1.0.0On a POSIX shell, the login command is:
printf '%s\n' "$WASMD_REGISTRY_TOKEN" | wasmd login https://registry.example.com --token-stdinUse an actual Wasm file in place of hello.wasm. Personal tokens can create
namespaces, upload artifacts, and publish or yank within their owner's namespaces.
They cannot administer the registry or mint additional tokens. Tokens are stored
only as hashes and can be revoked on the account page;
revocation takes effect on the next authenticated request.
On Your account → Personal access tokens, choose 7, 30, or 90 days, 1 year, a custom duration of 1–3650 days, or No expiration. The default remains 90 days. A non-expiring token remains valid until you revoke it. Expiration is chosen when the token is created; create a replacement and revoke the old token to change it. Existing tokens retain their original expiry, and browser sessions still expire after 24 hours. All active tokens, including non-expiring tokens, count toward the 30-credential limit per account (including sessions).
The browser-session API accepts an optional expires_in field:
| Request value | Behavior |
|---|---|
| Omitted | Expires after 90 days (backward compatible). |
| Integer from 1 to 315360000 | Expires after that many seconds, up to 3650 days. |
null |
No expiration; valid until revoked. |
For example, {"name":"GitHub Actions","expires_in":null} creates a non-expiring
token. Creation returns expires_in and expires_at; both are null when the
token never expires. Token listings return expires_at: null for those tokens.
Non-expiring tokens remain subject to namespace ownership and revocation, and
cannot create additional tokens. Scheduled cleanup preserves them. This change
uses the existing D1 schema; no manual data migration is required.
Create a token named GitHub Actions with the desired expiration. In your GitHub
repository, open Settings → Secrets and variables → Actions → New repository
secret and save it as WASMD_REGISTRY_TOKEN. Add a repository variable named
REGISTRY_URL containing your Worker origin, such as
https://registry.onebitbank.workers.dev.
The following workflow checks that the token can authenticate. Save it as
.github/workflows/check-registry.yml in the repository that will use the token:
name: Check registry access
on: workflow_dispatch
permissions: {}
jobs:
check:
runs-on: ubuntu-latest
steps:
- name: Verify registry credentials
env:
WASMD_REGISTRY_TOKEN: ${{ secrets.WASMD_REGISTRY_TOKEN }}
REGISTRY_URL: ${{ vars.REGISTRY_URL }}
run: |
test -n "$WASMD_REGISTRY_TOKEN"
test -n "$REGISTRY_URL"
curl --fail --silent --show-error \
--header "Authorization: Bearer $WASMD_REGISTRY_TOKEN" \
"${REGISTRY_URL%/}/v1/auth/check"For publishing, install the Wasmd CLI and build your artifact in the workflow,
then use the same environment secret with wasmd login --token-stdin and
wasmd push as shown above. Never commit the token or echo it in workflow logs.
See GitHub's guide to Actions secrets.
Namespace names and release coordinates have database uniqueness constraints. Publication uses a D1 transaction batch: a duplicate release rolls back the package description update as well. Concurrent uploads verify the same SHA-256 key before writing identical bytes. R2 is written before D1 metadata becomes visible. A failed database write may leave an unreferenced R2 object; this is safe to retry and is never exposed through the API until metadata exists. No automatic blob deletion or garbage collection runs.
Back up both D1 and R2. For example, export D1 with:
npx wrangler d1 export wasmd-registry --remote --output registry-backup.sqlReplace wasmd-registry with your actual database name if customized. Export by
database name because the source template omits the resolved database ID.
Copy the private R2 bucket using an S3-compatible backup client and keep the database and object snapshots together. Store exports securely because they contain password hashes and credential hashes. D1 Time Travel can restore metadata within the account's available window, but does not restore R2 objects.
There is no automatic import from legacy filesystem or SQLite deployments. Recreate accounts and namespace ownership and republish original artifacts and manifests through the API; copying legacy files into R2 does not import them.
/healthzchecks Worker liveness./readyzchecks a D1 application table and access to the R2 binding. It does not validate every schema table or verify every stored artifact.- Worker observability is enabled in
wrangler.jsonc. Inspect request failures in Cloudflare logs and correlate the API'srequest_idwith the logged error. - Monitor readiness failures, HTTP 5xx rates, rejected uploads, authentication throttling, D1/R2 usage, and Cloudflare billing.
The triggers.crons setting 17 * * * * invokes the scheduled handler at minute
17 of every hour (UTC). It removes expired sessions, personal tokens, login
throttle records, and OAuth states. It does not create database tables, run
migrations, or delete artifacts. Credential expiry is also checked on requests.
After restoring a backup, verify readiness, account access, namespace ownership, and an exact release plus the SHA-256 digest of every referenced artifact before switching traffic. Preserve both database metadata and the matching R2 objects.
- Workers Paid is required by the configured
cpu_ms: 30000. Argon2id and WIT parsing are CPU-intensive; local testing does not establish production throughput. - Uploads and inline analysis are bounded at 8 MiB, manifests at 64 KiB and
32 artifacts per publication to stay within Workers memory and D1 request limits.
Core modules, uninspectable components and larger components remain downloadable;
/componentreturns 404 when the artifact cannot be decoded as a component or WIT package. - External admission-policy processes and
/v1/admin/tokensendpoints are not supported. Use namespace-owned personal tokens. The served OpenAPI document contains the endpoints available in the Worker. - The UI, namespace ownership, registration, login, personal-token revocation, immutable publication, SHA-256 checking, Rust SemVer resolution, search, yank/unyank, WIT metadata, and CLI protocol are supported.
- Public registration enables anyone to store artifacts. Close registration when operating a private team registry. Storage quotas and billing remain managed by your Cloudflare account; this application does not implement per-user quotas.
Run npm test for type checking, a Wrangler deployment dry run and integration
tests in workerd with real local D1 and R2 bindings. Tests never deploy remotely.
Production deployment and the dashboard's
repository-cloning flow still require a public repository and your Cloudflare account.
References: Deploy buttons, Workers Builds image, Workers limits, D1 migrations.
| Component | Responsibility |
|---|---|
worker/index.ts |
HTTP routing, response headers, assets, health checks, and scheduled cleanup. |
worker/auth.ts and worker/github.ts |
Accounts, sessions, tokens, CSRF checks, throttling, and GitHub OAuth. |
worker/registry.ts |
Registry API, D1 metadata, and R2 artifact access. |
worker/core |
Rust validation, Argon2id, page rendering, SemVer, and Component/WIT analysis compiled to wasm32-unknown-unknown. |
worker/migrations |
Versioned D1 schema migrations applied before deployment. |
ui |
Embedded English HTML, CSS, JavaScript, and SVG assets; no separate frontend build. |
The browser application uses separate home, exploration, registration, login, account, publishing, documentation, package, and release routes. Ordinary links navigate between pages; JavaScript enhances forms and loads API data.
Headings use Georgia, "Times New Roman", serif; body text uses
"Segoe UI", "PingFang SC", "Microsoft YaHei", Arial, sans-serif, following the
font reference. These are system fonts and
require no font downloads. The project's component-cube SVG is shared by the
navigation, footer, and favicon.
The wasmd.package/v0 manifest is a distribution envelope, not a guest ABI.
Runtime capability grants and execution state do not belong in package metadata.
npm ci
npm testnpm test builds the shared Wasm helper, checks TypeScript, performs a Wrangler
deployment dry run, and runs integration tests in workerd with local D1/R2.
It does not deploy remotely or require Cloudflare credentials. Coverage includes
accounts, namespace ownership, immutable publication, digests, resolution,
yank/unyank, token revocation, component analysis, migration handling, and GitHub
OAuth state behavior. Automated OAuth tests use simulated GitHub responses;
verify a real authorization flow after configuring your own OAuth App.
Use npm run check for TypeScript checks and npm run build to rebuild the Worker
assets and Rust/Wasm helper. Add schema changes as new SQL files under
worker/migrations; keep existing applied migrations intact.
- OpenAPI source; use
/openapi.yamlon the deployed Worker for its supported endpoint set. - Security policy
- Contributing
- Deploy to Cloudflare announcement
The deployment button targets https://github.com/liusha-com/registry. Keep that
repository publicly accessible for one-click deployment, and update the button
URL if the repository moves or is renamed.
Apache-2.0.
