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.
(FOSS) A pull request is a submission
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.
Start from a working file. The scaffolder writes one; the app’s own Make section (wayBetter → Make) has examples and the live theme values.
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 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.
/**
* @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.
The Store is optional. A file that works is already shareable.
custom.css reloads in every tab at once.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.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/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
categoryreading, writing, filters, focus or tools. Themes: dark or light, taken from the theme’s mode when you leave it out.tagsversionThat 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.
There is no hosted CI. The checks are commands you run before you send, and a maintainer runs again before merging.
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
a-z 0-9 -, up to 40), name.plugin.js, no duplicates in an app@name, @version, a description, and a @permissions line that names network or network:nonenetwork:none is refused when the code calls fetch, XMLHttpRequest, WebSocket, EventSource or sendBeacon; gated API calls must be declaredeval, new Function or import(); no scripts or outside files in a preview; no @import or outside URLs in a theme’s CSSTwo ways in. Both need a free git.crt.fyi account; sign-up is open.
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.
One file for every wayBetter app. You never edit it by hand: it is built from the folders.
{ "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"] }
] }
apps. An app verifies the whole file and keeps only its own items.Not Store items: all 51 languages are built into the app and loaded on demand.
apps/desktop/locales/<tag>.json, by pull request or by an issue with the changed file attached.bun run i18n:check: keys, placeholders, plural forms, no markup. It must print 0 error(s).