Tintero Developers

Publishing a plugin

Packaging a plugin for other people, then getting it into the in-app plugin browser. What belongs in the zip, what gets dropped, and what the registry entry commits you to.

On this page

A published plugin is the same zip you have been reloading all along. Nothing gets signed.

There are two ways to hand it out, and they are worth keeping straight:

  • Give someone the zip. They install it through Settings → Plugins, and that is the whole process: no account, no review, no version number anybody checks.
  • Submit it to the registry, which puts it in the plugin browser inside the app, where people find it by browsing rather than by being handed a file. That costs a GitHub release and a pull request.

The packaging is the same either way, so start there.

What goes in the zip

Files are read from the top of the zip. One wrapping folder is fine, since the installer notices and strips it, but flat is simpler:

cd my-plugin && zip -FSr ../my-plugin.zip . -x '*.git*' 'node_modules/*'

If you have a build step, zip up what it produced, not your source:

npm run build && cp plugin.json icon.svg dist/ && cd dist && zip -r ../my-plugin.zip .

What survives installation

The manifest reference lists exactly which file types are kept and which are dropped, read out of the installer itself.

Anything it does not recognise is simply not unpacked. If your plugin needs a data file, make it .json.

Before you hand it to anyone

Install it from somewhere else. Not the copy you have been reloading all week. A fresh install puts you through the manifest checks and the permission dialog exactly as somebody else will see them.

Read your own permission dialog. It says precisely what you asked for. If a line on it surprises you, drop that permission. If it will surprise someone else, explain it in description.

Check the scary ones are actually needed. net.fetch , fs.write and the project.write.* family are the ones people hesitate over. Each should be there because your plugin genuinely cannot work without it.

Try it with no project open, then with a big one. The first catches missing null checks, since getFileContent() returns null for an empty file and there may be no open document at all. The second catches the rate limit, because looping over every file in a long manuscript is how most people meet 100 calls / 1 s.

Put in a license. Leaving it out shows up as a warning at install time, and people see it.

Set minTinteroVersion if you call anything added recently. A plugin that declines to install is much kinder than one that installs and then fails in ways that look like a permissions problem.

A pre-flight checklist

  • id is lowercase, written back to front the way a Java package is, and starts and ends with a letter or number.
  • version has moved on since the last copy you gave anyone.
  • icon is an .svg.
  • main ends in .js, or your stylesheet never loads.
  • registerPlugin() is called exactly once.
  • Every method you call has a matching permission, and every permission you ask for gets used.
  • network.domains names real hosts if you asked for net.fetch . A bare "*" fails the install.
  • onDeactivate() saves anything worth keeping and clears your timers.
  • The zip installs on a machine that has never seen it.

Versioning

version is just a string and nothing reads it as anything more, so semantic versioning is a habit here rather than a rule. It is the habit people expect, though, and minTinteroVersion makes more sense sitting next to one.

Permissions are remembered per plugin, so adding one in a new version means everybody sees the dialog again. Say so in your release notes. A dialog nobody was expecting is a dialog people say no to.

The registry

The plugin browser inside the app reads one JSON file from a public repository:

https://raw.githubusercontent.com/fnadalrod/Releases-Tintero/refs/heads/master/plugins/plugin-list.json

Your zip is not in it. Your entry points back at your own repository, and the app works out the download URL by dropping your repository and version into a fixed pattern:

${p.repository}/releases/download/${p.version}/plugin.zip

That settles three things which are otherwise easy to get wrong.

The release tag is your version string, exactly. No v in front, unless the version in your manifest starts with v too. Whatever you wrote is what gets looked up.

The files on the release must be called plugin.zip and plugin.json. Both of them. The browser reads the loose plugin.json so it can show your description without downloading the whole zip. A file named after your plugin is a file the app will never ask for.

The repository has to stay public. Installs fetch from it forever, not just once when you submit.

The entry

Fork fnadalrod/Releases-Tintero and add yourself to the list in plugins/plugin-list.json:

{
  "id": "com.example.word-count",
  "name": "Word Count",
  "repository": "https://github.com/you/word-count",
  "description": "A live word count for the document you are writing",
  "author": "Your Name",
  "version": "1.0.0",
  "type": "sidebar-panel",
  "tags": ["writing", "statistics"],
  "publishedAt": "2026-08-09",
  "updatedAt": "2026-08-09",
  "license": "MIT",
  "scopes": ["ui.sidebar", "project.read", "project.read.fileContent"]
}

Note that author is a plain string here, while in your manifest it is an object. Two different bits of code read these two files, and they do not share a shape.

A person reviews it by hand. There is nothing automated on that repository, so a malformed entry gets caught by a human or not at all.

Distributing without the registry

However you like: a GitHub release, a download on your own site, a link in a forum post. The four plugins that ship with Tintero each live in their own repository with the zip attached to a release, which is a sensible pattern to copy whether or not you ever submit yours.

Put this in your README: what it does, which permissions it asks for and why, the oldest Tintero it works on, and a screenshot. The permissions are the part people actually want explained.

Publishing a theme instead? Different registry file, different rules: publishing a theme.