Desktop field Plate 86 Route §86
Plate 86

The workspace with a keychain

Each project carries a credential. That credential does not go in the JSON file, and this is the passage that exists to say so.

projects.json gets synced, backed up, copied into a bug report, and read by anything on the machine. Secrets go to the OS keychain, keyed per project, and are deleted when the project is forgotten — an orphan keychain entry is the kind of thing nobody notices until a security review.

One trap, and it is silent: the macOS keychain hex-encodes a value containing a newline, which corrupts it on read. Keychain::put_json writes single-line JSON and asserts it in debug builds for exactly this reason.

What you are carrying

store::Keychain

One service per app, one entry per key.

host

The facade that opens the project and holds the cursor.

app

Home ⇄ workspace, two state scopes.

Start it

scripts/new.sh Acme ~/Dev/acme --template workspace

What it looks like

crates/store/src/lib.rsrust
impl Store {
    /// A project's secret. Never projects.json — that file gets synced,
    /// backed up, and pasted into bug reports.
    pub fn secret(&self, project_id: &str) -> Option<String> {
        self.inner.keychain().get(&format!("project.{project_id}"))
    }

    pub fn save_secret(&self, project_id: &str, secret: &str) -> bool {
        self.inner.keychain().put(&format!("project.{project_id}"), secret)
    }

    /// Called when a project is forgotten. An orphan entry outlives the app.
    pub fn drop_secret(&self, project_id: &str) {
        self.inner.keychain().delete(&format!("project.{project_id}"));
    }
}

Read next

Desktop architecture Gotchas
Type to search guides and package references.