Ship a GTK app as a Flatpak
Pick this route when you want a store listing on Flathub and a GTK version you control on every distro, rather than whatever the user’s distro happens to ship. It costs more setup than the other routes, which are compared on Ship your app. Shipping a headless tool instead? Go to Ship a CLI tool as a Flatpak.
Describe your app once
Section titled “Describe your app once”Everything the scaffold needs lives in package.json#gjsify.flatpak (or in
.gjsifyrc.*, they are merged). This is the smallest block that produces a
Flathub-acceptable result:
{ "name": "eu.jumplink.Learn6502", "version": "0.6.5", "type": "module", "gjsify": { "flatpak": { "appId": "eu.jumplink.Learn6502", "kind": "app", "runtime": "gnome", "runtimeVersion": "50", "command": "eu.jumplink.Learn6502",
"name": "Learn 6502 Assembly", "developer": { "id": "eu.jumplink", "name": "Pascal Garber" }, "summary": "Learn 6502 assembly", "description": "An interactive 6502 assembly learning environment.\n\nWrite, assemble and step through 6502 programs.", "license": { "metadata": "CC0-1.0", "project": "MIT" }, "homepageUrl": "https://github.com/JumpLink/easy6502", "bugtrackerUrl": "https://github.com/JumpLink/easy6502/issues",
"categories": ["Education", "Development"], "keywords": ["6502", "assembly", "emulator", "learning"], "releases": [{ "version": "0.6.5", "date": "2026-05-15" }],
"branding": { "accentLight": "#5b81b8", "accentDark": "#3a5d8c" }, "icon": "data/icons/hicolor/scalable/apps/eu.jumplink.Learn6502.svg",
"finishArgs": [ "--device=dri", "--share=ipc", "--socket=fallback-x11", "--socket=wayland" ] } }}A few of these are easy to get wrong:
nameis the display name people see. It defaults to something derived frompackage.json#name, which is right only when that is already your app id. Set it when your npm name islearn6502and your store name is “Learn 6502 Assembly”.developer.idis a reverse-DNS publisher id. AppStream requires it.summaryis one line, 80 characters or fewer, no trailing period.descriptionsplits on blank lines into paragraphs, and&,<and>are escaped for you.releasesneeds at least one entry matching the version you submit.iconpoints at a scalable SVG. Flathub rejects PNG-only icon sets.finishArgsare the sandbox permissions. The four above are the defaults forkind: "app", so you can leave the key out until you need more.
You do not need an SDK extension for a plain GJS app: the GNOME runtime already
carries GJS, GLib and libsoup, and gjsify build produces one self-contained
file. Add sdkExtensions only if you compile something inside the sandbox; each
org.freedesktop.Sdk.Extension.<name> you list also gets /usr/lib/sdk/<name>/bin
prepended to the build PATH automatically.
Generate the scaffold
Section titled “Generate the scaffold”gjsify flatpak init[gjsify flatpak init] wrote manifest: /home/you/learn6502/eu.jumplink.Learn6502.json[gjsify flatpak init] wrote metainfo: /home/you/learn6502/data/eu.jumplink.Learn6502.metainfo.xml.in[gjsify flatpak init] wrote desktop: /home/you/learn6502/data/eu.jumplink.Learn6502.desktop.in[gjsify flatpak init] wrote flathub.json: /home/you/learn6502/flathub.json| File | What it is |
|---|---|
<app-id>.json | The Flatpak manifest: runtime, SDK, finish-args, modules |
data/<app-id>.metainfo.xml.in | AppStream MetaInfo, required by Flathub |
data/<app-id>.desktop.in | The desktop entry, required for GUI apps |
flathub.json | Flathub policy file, {} for apps |
Each file is checked separately and skipped if it already exists, so re-running
init will not overwrite your hand-tuned .desktop. Pass --force when you
do want them regenerated.
If a required field is missing, the manifest is still written; the MetaInfo and
.desktop are held back and you get the exact key to fill in:
- gjsify.flatpak.developer.id: set { id: "org.example", name: "..." } - gjsify.flatpak.summary: short tagline ≤80 chars, no period - gjsify.flatpak.license.project: SPDX id like "MIT" or "GPL-3.0-or-later" - gjsify.flatpak.homepageUrl: e.g. "https://example.org"Fill the gaps and re-run with --force.
Rich AppStream features (i18n-ready)
Section titled “Rich AppStream features (i18n-ready)”The plain description: "string" form gets you paragraphs. When you want
bullet lists, screenshots, hardware hints or translator context, description
also accepts an array of blocks:
{ "gjsify": { "flatpak": { "summaryTranslatorHint": "App tagline shown in app stores",
"description": [ { "p": "Discover the fascinating world of 6502 assembly!", "translatorHint": "App store intro paragraph" }, { "ul": [ { "item": "Interactive tutorials guide you step by step", "translatorHint": "Tutorial feature bullet" }, "Built-in code editor with syntax highlighting" ] }, { "p": "Perfect for hobbyists, students, and curious minds." } ],
"screenshots": [ { "url": "https://example.com/screenshots/1.png", "caption": "Code editor and virtual game console", "captionTranslatorHint": "Screenshot of the main desktop layout" } ],
"kudos": ["ModernToolkit", "HiDpiIcon", "TouchscreenSupport"], "supports": { "controls": ["keyboard", "pointing", "touch"] }, "requires": { "displayLengthMin": 360 }, "recommends": { "displayLengthMin": 480 },
"contentRating": { "type": "oars-1.1", "attributes": { "social-info": "mild", "language-humor": "mild" } },
"translateUrl": "https://hosted.weblate.org/projects/your-project/app/",
"releases": [ { "version": "0.6.5", "date": "2026-05-15", "description": [ { "p": "GNOME 50 runtime support." }, { "ul": ["Updated TypeScript to v6", "Fixed back-button visibility"] } ] } ] } }}Every translatorHint becomes a <!-- TRANSLATORS: ... --> comment right
before its tag in the generated .metainfo.xml.in. xgettext and
msgfmt --xml --template forward those into the .po files, so people
translating on Weblate or Crowdin get the context without opening your repo.
developer.nameTranslatable defaults to false, which emits
<name translate="no">. That is what you want for a personal or brand name.
Set it to true when the name is a descriptive phrase.
The full field list is in the CLI Reference.
Check it before Flathub does
Section titled “Check it before Flathub does”gjsify flatpak check eu.jumplink.Learn6502.jsonThis runs the same two linters Flathub’s PR CI runs: appstreamcli validate --strict on your MetaInfo, and flatpak-builder-lint manifest on the
manifest. Add --repo <path> after a build to lint the exported repo as well.
A non-zero exit here means a red PR on Flathub, so fix it now. Both binaries
live in the org.flatpak.Builder Flatpak, and check prints the install hint
when they are missing:
flatpak install -y flathub org.flatpak.BuilderBuild and run it locally
Section titled “Build and run it locally”Flatpak builds have no network access, so dependencies have to be vendored first. Generate an offline source list from your lockfile:
gjsify flatpak sources --print-moduleThat reads gjsify-lock.json, package-lock.json, yarn.lock or
pnpm-lock.yaml, writes gjsify-sources.json, and with --print-module shows
you the manifest module to paste in: point XDG_CACHE_HOME at the vendored
cache and run gjsify install --immutable offline. No Node and no Python
helper involved.
If you already build with Node inside the sandbox, gjsify flatpak deps wraps
the flatpak-node-generator tool instead and writes
flatpak-node-sources.json.
Then build and install:
gjsify flatpak build eu.jumplink.Learn6502.json --installflatpak run eu.jumplink.Learn6502Now use it the way a user would: launch it from the GNOME overview, open a file dialog, switch to dark mode, check the icon in the app grid.
Wire up CI
Section titled “Wire up CI”gjsify flatpak ciWrites .github/workflows/flatpak.yml around
flatpak/flatpak-github-actions/flatpak-builder@v6, running inside
ghcr.io/flathub-infra/flatpak-github-actions:gnome-50. The container tag
follows your runtime and runtimeVersion. Re-running is a no-op when the
file is unchanged and refuses to clobber your edits without --force.
Submit to Flathub
Section titled “Submit to Flathub”- Tag the release:
git tag v0.6.5 && git push --tags. - Repoint the module’s source at that tag.
initwrites{ "type": "dir", "path": "." }, which builds your working copy: right for the local build above, not something Flathub can reproduce. The submitted manifest needs agitsource with atagand thecommitit resolves to. - Open a PR against
flathub/flathub
containing your
<app-id>.json. The MetaInfo and.desktopstay in your own source tree; the manifest installs them during the build. - The reviewer runs the same
appstreamcliandflatpak-builder-lintchecks you ran locally.
Keep Flathub current after a release
Section titled “Keep Flathub current after a release”Once you are on Flathub, your app has a second repo (flathub/<app-id>) whose
manifest pins your tag and commit. That is the git source from step 2:
sync-flathub rewrites it in place, so a manifest submitted with the dir
default stops it with no git source found in modules[0].sources.
gjsify flatpak diff # is Flathub behind your latest tag?gjsify flatpak sync-flathub # clone it, repoint the tag, open the PRgjsify flatpak release v0.6.6 # do the whole release in one gorelease chains the rest: regenerate the assets, run the linters, create and
push the tag, then open the Flathub PR. The linters run before the tag is
created, so a failure leaves you with no tag rather than a half-published
release. Add --dry-run to see the plan first.
sync-flathub needs git, and gh unless you pass --no-pr. If your Flathub
repo is not named flathub/<app-id>, set gjsify.flatpak.flathubRepo.
About icons
Section titled “About icons”Ship a scalable SVG at data/icons/hicolor/scalable/apps/<app-id>.svg. The
gjsify.flatpak.icon field points at it so init can warn when it is missing;
the actual install happens in your manifest’s modules, usually a Meson
install_subdir('icons') or an install -Dm644 line. Flathub rejects
PNG-only icon sets, so convert before you submit.
Reference
Section titled “Reference”gjsify flatpak initand every config key it readsgjsify flatpak checkgjsify flatpak build·sources·deps·cigjsify flatpak sync-flathub·diff·release- Flathub submission docs
- AppStream MetaInfo reference