# Marketplace — deferred work What `marketplace.officer.dev` and the plugin install path do not do yet, as of 2026-08-16. Design and rationale live in `docs/marketplace.md`; this is the list. Ranked by what actually blocks something. ## Blocks other people using it - [ ] **Third-party submissions cannot use the sync.** `scripts/marketplace-sync.ts` reads a manifest by IMPORTING it, which is code execution. Fine for our own repositories, not a mechanism a stranger's plugin can go through. Needs either a sandbox or a published `manifest.json` alongside the TypeScript one. Until this exists the catalogue is ours only, and everything below is cosmetic. - [ ] **No write API and no submission flow.** Registering a plugin means running the sync by hand against the database. Deliberate while nobody can submit; the first outside contributor makes it the gap. ## Blocks a plugin evolving - [ ] **Plugin installs CREATE, they do not MIGRATE.** `pushPluginSchema` generates the SQL that builds a schema from nothing and applies it, skipping objects that already exist. A new version that adds a column will not get it — the CREATE is skipped as "already exists" and the column is silently absent. Plugin upgrades need real migrations. The old global `db:push` appeared to handle this, at the price of being able to drop half the database, so this is a known trade rather than a regression. - [ ] **No versions table.** `plugin_metadata` is one row per plugin — a snapshot, not a history. A store that can offer "install 1.2 instead of 1.3", or show a changelog, needs a different shape. ## Staleness - [ ] **Nothing refreshes the catalogue automatically.** Publishing a plugin update changes nothing until somebody runs `bun --env-file=.env scripts/marketplace-sync.ts`. A Gitea webhook or a cron is the obvious next step. - [ ] **The sync reads local clones, not the remotes.** The machine running it must have every plugin checked out, so the catalogue describes that machine's copies. The version that clones each repo into a temp directory is a small change. - [ ] **`rawUrl()` assumes Gitea and a `main` branch.** A GitHub-hosted plugin, or one whose default branch is `master`, gets a 404 for its icon. The forge belongs in the registry rather than inferred from a URL shape. ## Scale - [ ] **`fetchCatalogue()` fetches everything, every time.** ~737 bytes per entry, so 100 plugins ≈ 72KB and 1,000 ≈ 720KB. Correct at three; the fix at that size is search rather than pagination, and search is a server-side query that changes the shape anyway — so this is deliberately not built early. The seam is `fetchCatalogue()` itself, which already returns a promise with three fallback paths behind it. - [ ] **The platform fetches the whole catalogue to answer "what can I install", then discards everything already on disk.** Fine at three, wasteful at 300. Moving that filter server-side means the platform telling the store what it has, which would be the first data sent TO the marketplace — worth doing deliberately rather than by accident. - [ ] **No search, no categories, no install counts.** Three plugins do not need them. ## Smaller - [ ] **A plugin's own icon is invisible before install.** `assets/` is published to `public/plugins//` on mount, which only happens once it is installed; the catalogue's `iconUrl` works around it by pointing at the repository, and a plugin author has to know to add one. - [ ] **`git`, `pm2` and `sudo` are still PATH lookups** in the install path. They work on both machines we have, and `bunx` did not — see `dc420ba5`. Anything new should follow `pushPluginSchema`'s pattern of resolving a path rather than trusting `$PATH`. - [ ] **Uninstall leaves the ecosystem file's PM2 entry removed but the process's logs behind.** Harmless, unexamined.