Bundling and publishing
Publish a working sandboxed plugin so other sites can install it. Publishing is sandboxed-only — native plugins distribute through npm.
Publish directly from the CLI, or use the automated release service to build and publish from GitHub Actions. Both paths write the release to your Atmosphere account. You only need a separate artifact host when you explicitly choose the direct CLI’s --url path.
Prerequisites
Section titled “Prerequisites”- A valid
emdash-plugin.jsoncwithslug,publisher,license, an author (authororauthors), and a security contact (securityorsecurityContacts). Runemdash-plugin validateto confirm. - A
version(inpackage.json, or the manifest for registry-only plugins). - An Atmosphere account to publish under.
Choose a publishing method
Section titled “Choose a publishing method”Both methods create publisher-owned package and release records. Choose where the release build should run and which credential should authorize it.
| Method | Use it when | Account access |
|---|---|---|
emdash-plugin publish |
You build and publish from your computer or another trusted environment. | The local CLI session writes the package profile, release, and blobs. |
| Automated releases | GitHub Actions should build releases from version tags or manual workflow runs. | The local CLI prepares the profile; the release service retains create-only release and blob authority. |
Your Atmosphere account
Section titled “Your Atmosphere account”You publish under an Atmosphere account: a portable, user-owned identity used across Bluesky and other apps in the AT Protocol network. One account is your single login across the network, with the same @handle everywhere, and your identity and data are not tied to any one app. EmDash uses this account as your publisher identity: every release you publish is a record in your own account, signed in as you.
EmDash uses the same Atmosphere accounts as its Atmosphere login for sites.
Use an existing account
Section titled “Use an existing account”If you already have a Bluesky account or any other Atmosphere account, sign in with its handle:
emdash-plugin login alice.bsky.socialThis opens your account provider’s sign-in page in the browser. EmDash never sees your password. emdash-plugin whoami lists your stored sessions; emdash-plugin switch <did> changes the active one.
Sign up for an account
Section titled “Sign up for an account”If you do not have an Atmosphere account yet, create one through any provider, then run emdash-plugin login <your-handle>. Your options:
- An app, such as Bluesky. Signing up for Bluesky creates an Atmosphere account hosted by Bluesky. This is the quickest route.
- An independent provider. Community-run or privacy-focused account hosts. Browse options at atmosphereaccount.com.
- Self-hosted. Run your own provider for full control over your identity and data.
Whichever you choose, the @handle from that account is what you pass to emdash-plugin login, and the account’s DID is what you pin as the publisher in your manifest.
Publish from your plugin directory
Section titled “Publish from your plugin directory”Log in once, then publish from the directory containing emdash-plugin.jsonc:
emdash-plugin login alice.example.comemdash-plugin publishpublish runs the same build and validation checks as bundle, creates the gzip archive, uploads it to your personal data server (PDS), uploads any declared listing images, and writes the release record.
When a canonical HTTPS repository is available, the command adds it to the package profile with optional provenance. Profiles without repository metadata also permit releases without provenance. If profile setup configured the package to require provenance, publish through the generated GitHub Actions workflow instead.
Bundle
Section titled “Bundle”bundle runs build, validates, collects assets, and creates a tarball. Inside the tarball, plugin.mjs is packed as backend.js (the filename the registry expects).
The command accepts the following flags:
emdash-plugin bundle [--dir <path>] [--out-dir|-o <path>] [--validate-only]| Flag | Default | Description |
|---|---|---|
--dir |
Current directory | Plugin source directory. |
--out-dir, -o |
dist |
Output directory for the tarball. |
--validate-only |
false |
Skip the tarball, but still produce dist/ artifacts. |
Tarball contents
Section titled “Tarball contents”| File | Required | Description |
|---|---|---|
manifest.json |
Yes | Generated manifest: id, version, capabilities, hosts, and the hooks and routes read from your source. You do not maintain this by hand. |
backend.js |
Yes | The built, self-contained runtime file (dist/plugin.mjs). |
README.md |
No | Plugin documentation. |
icon.png |
No | Conventional bundle icon. Must be a readable PNG; 256×256 is recommended. |
screenshots/ |
No | Up to eight .png, .jpg, or .jpeg files; 1920×1080 or smaller is recommended. |
Validation
Section titled “Validation”bundle (and --validate-only) check:
- Size caps (RFC 0001, decompressed): total ≤ 256 KB, per-file ≤ 128 KB, ≤ 20 files. The gzipped tarball is a fraction of that.
- No Node built-ins in
backend.js— sandbox code can’t importfs,path,child_process, etc. Use Web APIs, or move that logic to a native plugin. - Capability sanity — names must be in the recognised set.
- Trust-contract coherence — the
network:request/allowedHostscross-rules from Capabilities and hosts. - Conventional bundle assets — an unreadable
icon.pngor screenshot is skipped. The CLI warns when the icon is not 256×256 or a screenshot exceeds 1920×1080, but dimensions alone do not fail the bundle. Every included file still counts toward the file and decompressed-size caps.
To inspect the tarball before publishing, list its contents:
emdash-plugin bundletar tzf dist/my-plugin-1.1.0.tar.gzPublish
Section titled “Publish”Publish the current source and host its artifacts on your PDS:
emdash-plugin publishThe following manifest block adds listing images. Paths are relative to emdash-plugin.jsonc; PNG, JPEG, and WebP are supported.
{ "release": { "artifacts": { "icon": { "file": "./icon.png" }, "banner": { "file": "./banner.webp" }, "screenshots": [ { "file": "./screenshots/editor.png" }, { "file": "./screenshots/settings.jpg", "lang": "en" } ] } }}Manifest-declared listing images are separate from the conventional icon.png and screenshots/ files included in the tarball. Publishing uploads each declared image to the publisher’s PDS and writes its blob reference into the release record. Each image is limited to 1 MiB and 8,192 pixels in either dimension; a release can declare up to eight screenshots. See Release fields for the complete shape.
What publish does:
- Builds the plugin, validates the decompressed limits, and creates the gzip archive.
- Resumes your Atmosphere account session and checks publisher pinning.
- Confirms that the OAuth grant includes package and image blob scopes.
- Uploads the package and declared images to your PDS, then verifies each returned blob CID against the uploaded bytes.
- Creates the package profile on first publish and writes the immutable release record.
The CLI identifies the published package as @<publisher-handle>/<slug>, prints the public page that becomes available after approval, and gives an emdash-plugin info … --version <version> --watch command. That command reads the labeler’s current checks directly; unapproved package metadata remains absent from aggregator responses and the public plugin site.
If an existing login predates blob publishing, publish reports MISSING_BLOB_SCOPE. Run emdash-plugin logout, then log in again to approve the new scopes.
Use an external package URL
Section titled “Use an external package URL”Pass --url when the package bundle is already available over HTTPS or the account provider does not accept gzip blobs:
emdash-plugin publish --url https://downloads.example.com/gallery-1.0.0.tar.gzThe CLI downloads the URL, validates the served bundle, and computes its checksum. It does not upload the package blob on this path. Listing images still use PDS blobs.
To compare the hosted bytes with a local tarball, add --local:
emdash-plugin publish \\ --url https://downloads.example.com/gallery-1.0.0.tar.gz \\ --local dist/gallery-1.0.0.tar.gzVersions are immutable by default
Section titled “Versions are immutable by default”emdash-plugin publish refuses to replace an existing release at the same slug and version. Bump version before publishing again. The build reads version from package.json (see Keep one version value). Bump major for a broadened trust contract, minor for new hooks or routes, and patch for fixes.
Publisher mismatch
Section titled “Publisher mismatch”If publish fails with MANIFEST_PUBLISHER_MISMATCH, the active session is a different Atmosphere account than the manifest’s pinned publisher. Switch to the pinned account with emdash-plugin switch <did>, or update publisher in the manifest if you are genuinely transferring the plugin to a new account. See Use an existing account for managing sessions.
What to read next
Section titled “What to read next”- The
emdash-pluginCLI — every command - Automated plugin releases — publish from an approved GitHub Actions workflow
- The manifest — fields, trust contract, publisher pinning
- Capabilities and security