wayBetter Store

(FOSS) A pull request is a submission

Publish your own.

A theme is one JSON file. A plugin is one JavaScript file. Make it, try it in the app, then add it to the Store: one folder in one pull request, or the file in an issue if you do not use git. Both need a free git.crt.fyi account, and nothing else.

The wayBetter for X repository, where the Store’s folders live, becomes public with the release. Until then its links on this page will not open for you; the framework’s repository is already public, and everything in steps 1 and 2 works today.

1

Make it

Start from a working file. The scaffolder writes one; the app’s own Make section (wayBetter → Make) has examples and the live theme values.

A theme

Colors, fonts, corners, density, layout, background, effects. Three colors are enough; the rest is derived and text is kept readable.

Or build it in the app: Appearance → Theme Builder → Export. THEMES.md lists everything a theme can set.

A plugin

A class with start() and stop(), and a header that says who made it and what it uses.

Add --ts for a TypeScript project. PLUGINS.md has the API, the permissions and live reload.

hide-trends.plugin.js
/**
 * @name Hide Trends
 * @description Hides the trends box in the right column, so the timeline is the only thing moving.
 * @version 1.0.0
 * @author you
 * @permissions network:none
 */
module.exports = class HideTrends {
  start() { wayBetter.api.injectCSS('hide-trends', '[data-testid="sidebarColumn"] section { display: none }'); }
  stop()  { wayBetter.api.clearCSS('hide-trends'); }
};

The Store reads a plugin’s name, author, version, description and permissions from this header, and a theme’s name, author and description from its JSON. The install screen shows the same values, so the two can never disagree.

2

Try it

The Store is optional. A file that works is already shareable.

  • Drop the file on the wayBetter window. You get the same install screen a Store item gets: name, author, version, SHA-256, permissions, the code.
  • Turn on developer mode (Settings → Developer) while you work: a saved plugin, theme file or custom.css reloads in every tab at once.
  • Share it without the Store. Send the file, or a waybetter://install link to an https URL on waybetter.fyi, cdn1.crt.fyi or a raw file on git.crt.fyi. It is never installed without the install screen.
3

Add one folder

Every Store item is a folder under store/<app>/ in the repository. The folder name is the item’s id and its address here: /store/plugins/<id>/.

store/x/ — wayBetter for X’s items
store/x/plugins/hide-trends/
  hide-trends.plugin.js   the plugin, exactly as it installs
  item.json               { "category": "focus", "tags": ["trends", "sidebar"] }
  preview.svg             optional, 640×400 (.svg or .png); drawn for you without one

store/x/themes/midnight-mint/
  theme.json              the theme, as Appearance → Export writes it
  item.json               { "version": "1.0.0" }
  preview.svg             optional; drawn from the theme without one
category
Plugins: reading, writing, filters, focus or tools. Themes: dark or light, taken from the theme’s mode when you leave it out.
tags
Up to 12 words people would search for.
version
Themes only (a plugin’s is in its header). Raise it when you change the file.
Description
One or two sentences that say what it does, 50 characters or more. The first 160 become the search-engine description of its page.

That is the whole submission. The page, the card, the search entry, the link preview, the structured data and the sitemap entry are generated from the signed catalog when the site builds. store/README.md has the full rules.

4

Run the checks

There is no hosted CI. The checks are commands you run before you send, and a maintainer runs again before merging.

in your clone
git submodule update --init
bun install --frozen-lockfile
bun run check:store      # your folder, against the Store's rules
bun run test             # everything else, including the site's SEO test
  • Ids and namesfolder id (a-z 0-9 -, up to 40), name.plugin.js, no duplicates in an app
  • Sizeitem files up to 2 MiB, previews up to 300 KB at 16:10
  • The header@name, @version, a description, and a @permissions line that names network or network:none
  • Honest permissionsnetwork:none is refused when the code calls fetch, XMLHttpRequest, WebSocket, EventSource or sendBeacon; gated API calls must be declared
  • No remote codeno eval, new Function or import(); no scripts or outside files in a preview; no @import or outside URLs in a theme’s CSS
  • The catalogthe items build into a catalog the apps accept (the framework’s own rules), and the published catalog still matches its signature and hashes
  • Search and sharingpage title up to 60 characters, description 50 to 160, both unique
5

Send it

Two ways in. Both need a free git.crt.fyi account; sign-up is open.

Pull request

  1. Fork the repository on git.crt.fyi.
  2. Add your folder on a branch and run the checks.
  3. Open the pull request. The template asks what it does and what it uses.

No git? Open an issue

  1. Choose the Theme or Plugin template.
  2. Attach the file (and a preview, if you have one).
  3. A maintainer adds the folder for you.

Then a maintainer signs

The Store’s signing key is offline by design, so merging is not publishing. After the merge a maintainer reads the code, runs waybetter store build and waybetter store sign on the machine that holds the key, and pushes. The site rebuilds, your item gets its page, and every app sees it the next time it reads the signed catalog.

Anything in the signed catalog is vouched for by that key, which is why a person reads every plugin before it is signed.

{ }

The catalog format

One file for every wayBetter app. You never edit it by hand: it is built from the folders.

catalog.json (format 2)Open
{ "format": 2, "name": "wayBetter Store", "generatedAt": "2026-09-29T…Z", "items": [
  { "apps": ["x"], "kind": "plugin", "id": "thread-reader", "name": "Thread Reader",
    "author": "wayBetter", "version": "1.2.0", "description": "…",
    "category": "reading", "tags": ["threads"],
    "url": "https://waybetter.fyi/store/plugins/bt-thread-reader.plugin.js",
    "file": "bt-thread-reader.plugin.js", "sha256": "<64 hex>", "size": 10961,
    "preview": "https://waybetter.fyi/store/previews/plugin-thread-reader.svg",
    "permissions": ["ui", "clipboard", "commands", "network:none"] }
] }
  • Signed. catalog.json.sig is an Ed25519 signature over the exact bytes of catalog.json. Apps verify it with the public key compiled into them, then check every download’s size and SHA-256.
  • Strict. Unknown fields, more than 500 items or a file over 1 MiB refuse the whole catalog. store-catalog.schema.json is the JSON Schema; STORE.md in the framework has every rule.
  • Shared. Each item names its apps. An app verifies the whole file and keeps only its own items.
Aa

Languages

Not Store items: all 51 languages are built into the app and loaded on demand.

  • Fix or finish a translation in apps/desktop/locales/<tag>.json, by pull request or by an issue with the changed file attached.
  • Check it with bun run i18n:check: keys, placeholders, plural forms, no markup. It must print 0 error(s).
  • It ships with the next app update, not through the Store. TRANSLATING.md has the message syntax and how to see your words in the app.

See what is already there.

Every item in the Store shows its code. The bundled plugins are the best examples of how to use the API well.