Sitemap

TrueBlocks MiniDapp Preferences/Project Design

4 min readOct 2, 2025

How we structured preferences, projects, and persistence to keep our code clean, our users happy, and our future extensible.

When we first set out to build desktop applications using Go, Wails, React, and TrueBlocks, we followed the typical approach: we jumped straight into the code.

It worked. We even shipped some code.

Press enter or click to view image in full size

But then we wanted multiple windows. Then multiple projects. Then robust preferences. Then per-app configuration. Then undo. Then, a “recently used files” list. You get the idea.

Suddenly, what started as a clean, easy-to-build desktop app was turning into a tangle of disconnected user state, redundant flags, and logic that was anything but logical.

So we stopped.

We threw it all out and started from scratch — but this time we designed things first. No code. Just precise, structural planning ahead.

Part of the architecture that emerged from that work is explained in this article.

🧭 Requirements

We knew we needed to support:

  • Multiple different applications, built and deployed independently.
  • Within any given application, separate project or user files. Each project represents a related bit of work.
  • A clear split between preferences (configuration) and data (user projects).
  • Per-user and per-app configuration.
  • Local-only persistence — no external databases or servers, ever. We’re building a decentralized application after all.
  • A “single source of truth” in the backend, with a minimal frontend used only (mostly?) for presentation.
  • Easy support for undo, dirty state tracking, and reliable file saving. (Still working on this.)

🧱 Core Concepts — Configuration / Projects

Of course, each application handles its own collections of data, but for the core of the applications, the parts shared across all applications, we distilled our needs into four data structures:

  • OrgPreferences — Developer-level defaults (theme, log_level, etc.)
  • UserPreferences — Per-user, global overrides (theme, language)
  • AppPreferences — Per-app (last_project, recently_used_files)
  • Project — User-created data + project-specific preferences

Each app stores these as individual files locally:

# On Mac
$HOME
│─── ./Library/Application Support/TrueBlocks/
│ ├──org_prefs.yaml
│ ├──user_prefs.yaml
│ └──<application_name>/
│ └──app_prefs.yaml
└─── ./Documents/
└── MyProject.tbx

Preferences are stored in YAML (human-readable), and Projects Are Stored in JSON (application-owned).

🔁 The Preference Merge Model

At the heart of the design is a clean merge strategy:

org → user → app → project

Each layer overrides the one before it. This produces a single, flattened map[string]string in memory, keyed by logical names like:

{
"theme": "dark",
"language": "en",
"panel_position": "right"
}

We intentionally kept the keys flat and readable. The frontend doesn’t care where a value came from — it just asks for "theme" and gets the right value.

🔎 How the Merge Works

Press enter or click to view image in full size
All preferences are merged into the Project in order Org -> User -> App -> Project

Each layer updates the merged map. The final key-value set reflects the most specific level that defines each preference.

For example, if the user sets "theme" at the app level, it will override "theme" from the org level — but still be overridden if the project sets its own theme.

This merging happens automatically on every change.

🧠 Domain-aware Access

To let users override specific layers (e.g., theme at the user level), we allowed domain-prefixed keys:

SetPreference("theme", "dark")             // sets project-level default
SetPreference("user.theme", "light") // sets a user-wide override
GetPreference("org.language") // gets org-wide value

If no prefix is given, "project" is assumed.

The backend resolves these intelligently. The frontend simply calls one function.

🧊 The Central State Object

All data lives in the backend in a single *App object:

type App struct {
Org OrgPreferences
User UserPreferences
App AppPreferences
Project Project
Merged map[string]string
Dirty bool
FilePath string
}

All changes funnel through Set* functions that automatically update the dirty flag and re-merge preferences. The frontend is a passive presenter, and the backend never loses track of App state.

💾 File Operations, Done Right

We implemented four file actions:

  • FileSave() — saves to the current file
  • FileSaveAs(path, confirmOverwrite) — saves to a new file, prompts if the file already exists
  • FileOpen(path) — loads an existing file from disk
  • FileNew() — creates a blank project

The frontend handles any UI prompts; the backend simply enforces integrity.

We also track:

  • A dirty flag (to prevent unsaved data loss)
  • A recently used files list (per app)
  • A last opened project field (for a startup resume)

All changes are persisted with zero surprises.

🧭 Final Thoughts

This architecture gave us:

  • One single source of truth
  • Per-user and per-project separation
  • Predictable, testable persistence
  • No risk of confusion between app state and saved state
  • A backend-first flow that takes advantage of Go’s type safety and speed

We’re building multiple production-grade apps on top of this architecture now — and it’s holding up perfectly.

If you’re designing a local-first desktop app — or simply want to build something pleasant to maintain — I hope this provides a solid foundation to build upon.

Please let us know how you’d extend, simplify, or challenge it. We’d love to hear.

Your Support is Welcome

TrueBlocks is funded from personal funds and grants from The Ethereum Foundation (2018, 2022, 2024), Optimism Retro PGF (2022, 2023), Moloch DAO (2021), Filecoin/IPFS (2021), Consensys (2019), and our lovely GitCoin donors.

If you like this article and wish to support our work, please donate to our GitCoin grant using ETH or any token. If you’re an Optimism badge holder, vote for our project in future Retro rounds. Or send us a tip directly at trueblocks.eth or 0xf503017d7baf7fbc0fff7492b751025c6a78179b.

--

--

Thomas Jay Rush
Thomas Jay Rush

Written by Thomas Jay Rush

Blockchain Enthusiast, Founder TrueBlocks, LLC and Philadelphia Ethereum Meetup, MS Computer Science UPenn

No responses yet