Chapter 9 of 15

Settings, secrets and work in the background

Freddy's app now remembers his theme, keeps a token where tokens belong, exports without freezing the window and empties the bin on a schedule. Each of these is a Laravel facade he already knows, scaled down to one process on one machine.

Settings: the store

For settings and light state there is a key-value store, always available, no feature flag. Values are any JSON, saved to a settings.json in the OS config directory:

import { store } from "@elyra/runtime";

await store.set("theme", "dark");
await store.set("lastNotebook", 3);
const theme = await store.get("theme");   // "dark" | null

Writes are atomic, through a temporary file and a rename, with the previous good version kept as settings.json.bak and used if the main file cannot be parsed. A store set on every keystroke does not write the file per keystroke, because flushes are debounced. store.clear() from the page needs a capability that is opt-in. Notes themselves stay in the database: the store is for settings.

Secrets: the keychain

A token does not belong in a clear-text settings file. With the secrets feature, credentials go to the macOS Keychain, the Windows Credential Manager or the Linux Secret Service:

App::new().provider(SecretsProvider::for_app("Freddy Notes"))

// in a #[command]
let secrets = ctx.get::<Secrets>();
secrets.set("sync_token", &token)?;
let token = secrets.get("sync_token")?;   // Option<Secret>

A read returns a Secret, not a String: it is used like a string, prints Secret(***) in a stray log line, and wipes its bytes when dropped. There is deliberately no route to secrets from the page, because a value any script in the webview can read is not a secret: read it in a command and return only what the UI needs.

Not for AI provider keys. The secrets page is blunt about it: a key inside a distributed desktop binary is a key your users have. Keep provider keys on a server and use Secrets for the user's own credentials. Chapter 10 comes back to this.

A disk and a cache

The storage facade is Laravel's Storage:: over a local directory, with every path jailed to its root, so .. and absolute paths are refused. Freddy roots it in his app's data folder, and his exports go to exports/ on it. The cache is Cache:: in one process, TTL-aware, with remember, add, counters and the rest, which suits the word counts on his dashboard:

App::new()
    .provider(StorageProvider::at(&data_dir))
    .provider(CacheProvider);

let stats = ctx.get::<Cache>().remember("stats", Some(Duration::from_secs(60)), || compute_stats());

The queue

Exporting a thousand notes should not happen on the UI's time. The queue is Laravel's Queue:: for one process: push a named job, register a handler in a provider's boot, and it runs on a background task.

struct Jobs;
impl Provider for Jobs {
    fn boot(&self, ctx: &Ctx) {
        ctx.get::<Queue>().on("export_notes", |payload| async move {
            // write the files to the storage disk
            Ok(())
        });
    }
}

App::new().provider(QueueProvider::with_workers(2).durable()).provider(Jobs);

In memory, jobs are lost on exit; durable() keeps every job in the app's database until it succeeds or finally fails, so an export queued before Freddy quits runs at the next launch, with no migration to write. A job can have attempts, a backoff and a timeout, failed jobs are listed and can be retried, and the queue is bounded, so push answers false rather than growing until the process dies. The page can push jobs too and watches their status through onQueue; the job code itself stays in Rust, as Laravel's jobs stay on the server.

The scheduler

let sched = ctx.get::<Scheduler>();
sched.daily_at("03:00", "empty_bin", || async { /* force-delete notes trashed 30 days ago */ });

The scheduler is Laravel's Schedule, running for the life of the app. One detail catches Laravel developers: intervals are measured from app start, so daily() means every 24 hours, not at midnight. For a time of day, as Freddy wants, there are clock jobs such as daily_at, weekdays_at and weekly_on.

The guides are Settings store, Secrets, Storage, Cache, Queue and Scheduler.

What you learned

  • The store keeps settings as JSON with atomic writes and a backup; the database keeps data.
  • Secrets uses the OS keychain, returns a Secret that cannot leak through a log, and is never reachable from the page.
  • AI provider keys do not belong in a desktop binary at all.
  • The queue runs jobs in the background, durably if asked; the scheduler's intervals count from app start, and clock jobs run at a time of day.
Next: in Chapter 10 Freddy adds AI to the app, and lets AI agents into it.