The XDG Contract
Where Linux apps put configuration, data, state, caches, desktop entries, icons, MIME types, and autostart files — and why the XDG specs are the closest thing the desktop has to a stable API.
Linux has no single per-user application-support directory. It has a small set of environment variables and directory conventions maintained by freedesktop.org. They are boring, underspecified in places, and absolutely essential.
If your app writes to the right place, backups include it, resets remove it, sandboxes map it, and users can find it. If it writes next to the binary, into ~/.myapp, or into the current working directory, all of that breaks — often silently, often only after a user upgrades.
The base directories
Section titled “The base directories”| Variable | Default | Put here |
|---|---|---|
XDG_CONFIG_HOME |
~/.config |
User preferences and configuration |
XDG_DATA_HOME |
~/.local/share |
User data that should be backed up |
XDG_STATE_HOME |
~/.local/state |
Logs, session state, recent history |
XDG_CACHE_HOME |
~/.cache |
Regenerable data; safe to delete |
XDG_RUNTIME_DIR |
/run/user/$UID |
Sockets, locks, pipes; cleared on logout |
XDG_DATA_DIRS |
/usr/local/share:/usr/share |
System-wide data |
XDG_CONFIG_DIRS |
/etc/xdg |
System-wide configuration defaults |
The XDG_RUNTIME_DIR is created by pam_systemd with mode 0700. Do not create it yourself; check that it exists and fall back to a private temporary directory only if necessary.
A healthy app directory
Section titled “A healthy app directory”~/.config/notes/└── settings.ini # small, human-editable preferences
~/.local/share/notes/├── notes.db # user data└── templates/ # user-visible assets
~/.local/state/notes/├── session.json # window layout, last opened note└── logs/ └── notes.log # rotated, no secrets
~/.cache/notes/└── thumbnails/ # regenerable, safe to deleteIf the app is sandboxed, these paths are remapped inside the sandbox. A Flatpak app’s ~/.local/share/notes becomes ~/.var/app/io.github.yourname.Notes/data/notes, and the host paths are not visible unless the user granted them. That is the point.
Configuration: GSettings or a file?
Section titled “Configuration: GSettings or a file?”Use GSettings when the settings should integrate with the desktop, be discoverable, and be schema-validated. Use a simple config file when the settings are app-private, highly dynamic, or need to be edited outside the app.
| Approach | Best for | Notes |
|---|---|---|
| GSettings / dconf | Desktop-integrated preferences | Schema XML, glib-compile-schemas, Gio.Settings |
| INI/TOML/JSON | App-private config, portable builds | Write atomically; validate; version the format |
| SQLite | Structured data, search, migrations | Use WAL mode; handle corruption |
Keyfile (GKeyFile) |
Small, human-readable config | GLib-native, no extra dependency |
Do not mix all four. Pick one primary store and document the migration path.
GSettings example
Section titled “GSettings example”<?xml version="1.0" encoding="UTF-8"?><schemalist> <schema id="io.github.yourname.Notes" path="/io/github/yourname/Notes/"> <key name="window-width" type="i"> <default>980</default> <summary>Window width</summary> </key> <key name="window-height" type="i"> <default>680</default> <summary>Window height</summary> </key> <key name="sort-order" type="s"> <default>'modified'</default> <summary>Sort order for notes</summary> <choices> <choice value="modified"/> <choice value="title"/> </choices> </key> </schema></schemalist>self.settings = Gio.Settings.new(APP_ID)
# On startup: default-width/default-height are the initial size.self.settings.bind("window-width", self, "default-width", Gio.SettingsBindFlags.DEFAULT)self.settings.bind("window-height", self, "default-height", Gio.SettingsBindFlags.DEFAULT)
# On close: save the actual size, not the initial size.def on_close_request(self, _window): self.settings.set_int("window-width", self.get_width()) self.settings.set_int("window-height", self.get_height()) return Falsedefault-width/default-height are not updated when the user resizes the window; save get_width()/get_height() on close and restore them with set_default_size() or the bindings above.
Install the schema to $datadir/glib-2.0/schemas/ and run glib-compile-schemas. For local development, set GSETTINGS_SCHEMA_DIR to the directory containing your compiled schema.
Desktop entries
Section titled “Desktop entries”Every graphical app needs a .desktop file. It is how the app appears in the app grid, the software center, and the file manager’s “Open With” menu.
[Desktop Entry]Name=NotesGenericName=Note TakingComment=Write and organize notesExec=notes %UIcon=io.github.yourname.NotesTerminal=falseType=ApplicationCategories=Office;MimeType=text/markdown;text/plain;Keywords=notes;writing;markdown;StartupNotify=trueStartupWMClass=notesDBusActivatable=true| Field | Why it matters |
|---|---|
Exec |
Must match the installed binary; %U passes files/URLs |
Icon |
Must match the installed icon name, not a file path |
Categories |
Used by menus and software centers; follow the spec |
MimeType |
Declares what the app can open; use xdg-mime to verify |
StartupNotify |
Lets the compositor show a launch indicator |
StartupWMClass |
Matches the window to the desktop entry; GTK derives WM_CLASS from the program name unless you set it, so test with xprop WM_CLASS |
DBusActivatable |
Lets the desktop start the app through D-Bus; ship a matching service file with --gapplication-service |
NoDisplay |
Hides helper entries from menus |
OnlyShowIn / NotShowIn |
Restricts to specific desktops; use sparingly |
Validate it:
desktop-file-validate io.github.yourname.Notes.desktopDo not put shell pipelines or environment-specific paths in Exec. If you need environment variables, use a wrapper script or Exec=env FOO=bar notes. Keep the command simple and consistent with the installed binary; %U passes files or URLs.
MIME types
Section titled “MIME types”The shared MIME-info database tells the desktop how to identify files and which app should open them. For common formats (text/plain, image/png, application/pdf), you only need to declare MimeType= in the desktop file.
For a custom format, install an XML file under $datadir/mime/packages/ and run update-mime-database:
<?xml version="1.0" encoding="UTF-8"?><mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info"> <mime-type type="application/x-notes-document"> <comment>Notes document</comment> <glob pattern="*.notes"/> <magic priority="60"> <match type="string" value="NOTES" offset="0"/> </magic> </mime-type></mime-info>sudo update-mime-database /usr/share/mimexdg-mime query default application/x-notes-documentPrefer an existing MIME type when one fits. A custom type is a commitment: you need an icon, a description, and a fallback for apps that do not know it.
Install icons into the hicolor theme:
/usr/share/icons/hicolor/scalable/apps/io.github.yourname.Notes.svg/usr/share/icons/hicolor/256x256/apps/io.github.yourname.Notes.png/usr/share/icons/hicolor/symbolic/apps/io.github.yourname.Notes-symbolic.svg- The name must match the desktop entry’s
Icon=value. - Provide a scalable SVG and at least a 256×256 PNG fallback.
- A symbolic variant is useful for menus and buttons; keep it monochrome and legible at 16 px.
- Do not install icons into an app-private directory unless the desktop entry points to an absolute path (which is fragile).
Autostart
Section titled “Autostart”Autostart entries live in $XDG_CONFIG_HOME/autostart/. They are normal desktop files. A user can disable one by creating an override with Hidden=true; do not ship Hidden=true in the app’s own autostart file, because that disables it for everyone.
[Desktop Entry]Type=ApplicationName=NotesExec=notes --backgroundIcon=io.github.yourname.NotesX-GNOME-Autostart-enabled=trueNoDisplay=trueAutostart is a user choice, not a default. If the app needs to run in the background, offer an explicit switch in settings. In a sandbox, use the Background portal rather than writing to the host autostart directory.
Trash, recent files, and user directories
Section titled “Trash, recent files, and user directories”- Trash: use
Gio.File.trash()or the Trash portal. Never permanently delete a user file without an explicit confirmation. - Recent files: use
Gtk.RecentManageron GTK orKRecentFilesActionon KDE so the file appears in the desktop’s recent list. Do not maintain a private recent-files list. - User directories: use
g_get_user_special_dir()orxdg-user-dirinstead of assuming~/Documents. The localized paths are defined in~/.config/user-dirs.dirs, but the API is the stable interface.
System-wide installation
Section titled “System-wide installation”| Asset | Install to |
|---|---|
| Executable | $prefix/bin |
| Desktop entry | $prefix/share/applications |
| AppStream metainfo | $prefix/share/metainfo |
| Icons | $prefix/share/icons/hicolor/... |
| GSettings schema | $prefix/share/glib-2.0/schemas |
| MIME package | $prefix/share/mime/packages |
| D-Bus service file | $prefix/share/dbus-1/services |
| systemd user unit | $prefix/lib/systemd/user |
| Translations | $prefix/share/locale |
| Documentation | $prefix/share/doc/$app-id |
| License | $prefix/share/doc/<package>/copyright |
Use your build system’s install directories (get_option('datadir') in Meson, GNUInstallDirs in CMake) instead of hardcoding /usr/share.
Migration and compatibility
Section titled “Migration and compatibility”Apps evolve. When the storage format changes:
- Read the old format if it exists.
- Migrate in memory or into a new file.
- Write the new format atomically.
- Keep a backup until the migration succeeds.
- Never delete the old data on first launch without a fallback.
When moving from ~/.myapp to XDG paths, look for the old directory once, copy the data, and leave a note in the log. Do not silently abandon the user’s history.
Validation checklist
Section titled “Validation checklist”- ☐ No hardcoded
~/.config,~/.local/share, or~/.myapppaths. - ☐ Config, data, state, cache, and runtime files are separated.
- ☐ Writes are atomic; partial writes cannot corrupt data.
- ☐ The desktop file passes
desktop-file-validate. - ☐ The metainfo file passes
appstreamcli validate. - ☐ The icon name matches the desktop entry and the app ID.
- ☐ The app declares the MIME types it can open.
- ☐ Autostart is opt-in and uses the Background portal when sandboxed.
- ☐ Recent files and trash use the desktop APIs.
- ☐ A reset removes config and state but not user data without confirmation.
Sources
Section titled “Sources”- XDG Base Directory
- Desktop Entry
- Shared MIME-info
- Icon Theme
- Autostart
- XDG User Dirs
- Debian Policy
- GSettings
Next: System Integration — D-Bus, notifications, secrets, search, and the services that make an app feel part of the desktop.