Appendix B: Making It a Real Application
Appendix A left you with a test suite. Nothing here changes what the app does. This appendix closes the gap between an app that runs from your project directory and one that installs, appears in the overview under its own icon, and carries the metadata a software center expects.
What the build produces
Leave the dev server running. gtkx build writes to dist/, which the dev server never reads, so they coexist.
npm run build> tasks@0.0.1 build
> gtkx build
[gtkx] Building ~/tasks/src/index.tsx
vite v8.2.1 building ssr environment for production...
[gtkx] Queued GSettings schema: com.gtkx.tutorial.gschema.xml
[gtkx] Compiled 1 GSettings schema(s)
[gtkx] Copied 2 icon(s) into icons/
✓ 256 modules transformed.
rendering chunks...
computing gzip size...
dist/icons/hicolor/symbolic/apps/com.gtkx.tutorial-symbolic.svg 0.49 kB │ gzip: 0.28 kB
dist/gschemas.compiled 0.63 kB
dist/icons/hicolor/scalable/apps/com.gtkx.tutorial.svg 1.47 kB │ gzip: 0.38 kB
dist/gtkx-packages.json 1.94 kB │ gzip: 0.35 kB
dist/gtkx.node 1,624.69 kB
dist/bundle.mjs 4,067.30 kB │ gzip: 510.94 kB
✓ built in 694ms
[gtkx] Build complete: dist/bundle.mjsEverything except the bundle is found at runtime relative to the bundle itself: it prepends its own directory to GSETTINGS_SCHEMA_DIR and XDG_DATA_DIRS, and loads gtkx.node from beside itself. Keep them together and the app is self-contained. Move bundle.mjs on its own and the settings schema goes missing on the first useSetting call.
node dist/bundle.mjs runs the app on any machine with GTK4, Adwaita, and Node.js 24 installed. That works, but it is not yet something a user can double-click.
Icons
The build copies data/icons/ verbatim, so the layout you write is the layout that ships. Use the same shape as the system icon theme:
data/icons/hicolor/scalable/apps/com.gtkx.tutorial.svg
data/icons/hicolor/symbolic/apps/com.gtkx.tutorial-symbolic.svghicolor is the fallback theme every icon theme inherits from, scalable is where SVGs go, and apps is the context. The file name is the application ID, which both the desktop entry's Icon key and the About dialog's applicationIcon prop look up.
The full-color icon is a 128 by 128 SVG. The symbolic variant is a separate 16 by 16 drawing in a single flat fill, so the desktop can recolor it for a dark header bar or a notification badge.
data/icons/hicolor/symbolic/apps/com.gtkx.tutorial-symbolic.svg:
<?xml version="1.0" encoding="UTF-8"?>
<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
<path d="m 5 1 c -2.216 0 -4 1.784 -4 4 v 6 c 0 2.216 1.784 4 4 4 h 6 c 2.216 0 4 -1.784 4 -4 v -6 c 0 -2.216 -1.784 -4 -4 -4 z m 0 2 h 6 c 1.108 0 2 0.892 2 2 v 6 c 0 1.108 -0.892 2 -2 2 h -6 c -1.108 0 -2 -0.892 -2 -2 v -6 c 0 -1.108 0.892 -2 2 -2 z" fill="#241f31"/>
<path d="m 4.5 8.5 l 2.5 2.5 l 5.5 -5.5 l -1.5 -1.5 l -4 4 l -1 -1 z" fill="#241f31"/>
</svg>Telling the desktop about the app
A desktop entry makes the app something the desktop knows about rather than a path you type, and an AppStream metainfo file is what a software center reads. Both are generated, from one block in gtkx.config.ts:
deploy: {
name: "Tasks",
genericName: "Task Manager",
binaryName: "gtkx-tutorial",
summary: "Manage your tasks and to-dos",
description: [
"A task manager built with GTKX, demonstrating how to build React-based GTK4 and Adwaita "
+ "desktop applications.",
"It shows an adaptive sidebar layout, boxed lists, a task editor, GSettings-backed preferences, "
+ "undo toasts, drag-to-reorder, desktop notifications, and local JSON persistence.",
],
categories: ["Office", "ProjectManagement"],
keywords: ["Task", "Tasks", "Todo", "To-do", "Checklist"],
developer: { id: "dev.gtkx", name: "GTKX", email: "hello@gtkx.dev" },
homepage: "https://gtkx.dev",
urls: {
bugtracker: "https://github.com/gtkx-org/gtkx/issues",
"vcs-browser": "https://github.com/gtkx-org/gtkx",
},
screenshots: [
{ file: "assets/screenshot.png", caption: "Browsing task lists in the sidebar", isDefault: true },
{ file: "assets/screenshot-editor.png", caption: "Editing a task" },
],
releases: [{ version: "1.0.0", date: "2026-07-13", notes: ["Initial release."] }],
branding: { light: "#3584e4", dark: "#1a5fb4" },
contentRating: {},
isDbusActivatable: true,
desktopEntry: { "X-GNOME-UsesNotifications": "true" },
targets: ["flatpak", "deb", "rpm", "appimage"],
},Most of it is optional. name, version, license, developer, and homepage all fall back to package.json, and summary falls back to its description, so the smallest working block is a summary and a categories list. The tutorial spells everything out because it doubles as a reference.
A few keys earn their place. X-GNOME-UsesNotifications gives the app its own row in the desktop's notification settings, so the reminders from Reminders That Reach the Desktop can be tuned or silenced there. isDbusActivatable lets the desktop start the app over D-Bus instead of running the command directly, which is how a reminder's Mark Complete button reaches the app.complete-task action when the app is closed.
categories also decides where the app appears in a launcher that groups by category, and it is what the deb Section and the rpm Group are derived from.
One command
npm run deploy[gtkx] Deploying Tasks 1.0.0-1 as gtkx-tutorial (x86_64) to appimage, deb, flatpak, rpm
[gtkx] Validated the desktop entry and the metainfo
[gtkx] Building ~/tasks/src/index.tsx
[gtkx] Build complete: dist/bundle.mjs
[gtkx] Bundled Node.js v24.19.0 (100.8 MiB, glibc >= 2.28)
[gtkx] Staged 12 files into build/stage
[gtkx] Wrote build/targets/appimage/AppRun
[gtkx] Wrote build/targets/deb/nfpm.yaml
[gtkx] Wrote build/targets/flatpak/com.gtkx.tutorial.yml
[gtkx] Wrote build/targets/rpm/nfpm.yaml
[gtkx] Built build/out/Tasks-1.0.0-x86_64.AppImage (36.6 MiB)
[gtkx] Built build/out/gtkx-tutorial_1.0.0-1_amd64.deb (40.2 MiB)
[gtkx] Built build/out/com.gtkx.tutorial-1.0.0-x86_64.flatpak (31.2 MiB)
[gtkx] Built build/out/gtkx-tutorial-1.0.0-1.x86_64.rpm (40.1 MiB)
[gtkx] Deploy complete: 4 artifacts in build/outThe desktop entry and the metainfo are validated in the second step, before the build, so a bad category or a missing summary fails in about two seconds rather than after everything else has run.
Note the third line: Node.js is bundled into the package. GTKX needs Node.js 24, Debian 13 ships 20, and Ubuntu 26.04 ships 22, so the package cannot depend on the distribution's. gtkx deploy fetches the official build matching yours, verifies its checksum, and caches it, which is where most of each package's size comes from.
What is inside
Every package installs the same tree, under /usr here and under /app in the Flatpak, apart from where the notices land:
/usr/bin/gtkx-tutorial a launcher script
/usr/lib/gtkx-tutorial/node the bundled Node.js
/usr/lib/gtkx-tutorial/bundle.mjs the app
/usr/lib/gtkx-tutorial/gtkx.node the native addon
/usr/lib/gtkx-tutorial/gschemas.compiled the compiled schema
/usr/share/applications/com.gtkx.tutorial.desktop generated
/usr/share/dbus-1/services/com.gtkx.tutorial.service generated
/usr/share/metainfo/com.gtkx.tutorial.metainfo.xml generated
/usr/share/icons/hicolor/**/apps/com.gtkx.tutorial.svg from data/icons
/usr/share/glib-2.0/schemas/com.gtkx.tutorial.gschema.xml from data/
/usr/share/licenses/gtkx-tutorial/LICENSE your LICENSE, every target but deb
/usr/share/licenses/gtkx-tutorial/THIRD-PARTY-NOTICES generated, every target but deb
/usr/share/doc/gtkx-tutorial/copyright generated, deb onlyThe last three are the terms of everything in the package that you did not write: the bundled Node.js, GTKX, and every npm package the bundle reaches. The deb carries them in the machine-readable copyright format that Debian expects, and the other targets carry the same material as a plain text file. See Deploying.
The launcher resolves the other files from its own location, so nothing is hardcoded to /usr and the same tree works in a Flatpak and inside an AppImage.
Installing it
sudo apt install ./build/out/gtkx-tutorial_1.0.0-1_amd64.deb
sudo dnf install ./build/out/gtkx-tutorial-1.0.0-1.x86_64.rpm
flatpak install --user ./build/out/com.gtkx.tutorial-1.0.0-x86_64.flatpak
chmod +x build/out/Tasks-1.0.0-x86_64.AppImage && ./build/out/Tasks-1.0.0-x86_64.AppImageOpen the overview and type Tasks. The app appears with its own blue checklist icon rather than a generic placeholder, and pressing Enter launches it. Open the desktop's notification settings and Tasks is listed there as an app that sends notifications.
The deb and the rpm install no maintainer scripts. Refreshing the desktop, icon, and schema caches is left to the distribution's own triggers, which is what a well-behaved package does.
Reviewing what was generated
npm run deploy -- --print-manifestswrites and validates the metadata and every target's manifest, then stops without packaging. build/metadata/com.gtkx.tutorial.desktop comes out as:
[Desktop Entry]
Type=Application
Name=Tasks
GenericName=Task Manager
Comment=Manage your tasks and to-dos
Exec=gtkx-tutorial
Icon=com.gtkx.tutorial
Terminal=false
Categories=Office;ProjectManagement;
Keywords=Task;Tasks;Todo;To-do;Checklist;
StartupNotify=true
StartupWMClass=com.gtkx.tutorial
DBusActivatable=true
X-GNOME-UsesNotifications=trueThe identifiers agree because they come from one source: Icon is the application ID from Your First Window, Exec is binaryName, and the metainfo's launchable points back at this file.
When a tool is missing
gtkx deploy needs desktop-file-validate and appstreamcli for the metadata, and flatpak-builder for the Flatpak. It lists everything missing at once, with the install line for your distribution:
[gtkx] error Cannot deploy: 1 required tool is missing.
flatpak-builder builds the Flatpak
sudo dnf install flatpak-builder
or: flatpak install --user -y flathub org.flatpak.Builder
Narrow the run with --target if you do not need every package format.nfpm for the deb and rpm, and appimagetool for the AppImage, are downloaded and checksum-verified on first use, so building a .deb on Fedora needs nothing installed.
Next
Appendix C submits the Flatpak to a store that builds it from source: Shipping It on Flathub.