README is a short front page (about 120 lines) with one real visual per section; every former section moved unchanged into docs/*.md with a docs/README.md index (journey, rejections, getting-started, commands, growth, studios, store-art, listing-languages, api, skills, mcp, safety, faq, project). The site tells the same story on one page: the teaser in the hero, real CLI output as text, the desktop app as a section with light/dark captures of eight screens and the per-system downloads (desktop/latest.json); /desktop/ redirects to /#download. New capture tools: tools/site-desktop-shots.ts (fixture-mode desktop frames) and tools/site-term-shots.ts (real CLI output as text and WebP). tests/docs-links.test.ts checks README, docs and site links resolve.
33 KiB
storebound desktop (apps/desktop)
A Tauri 2 window around the storebound CLI: every app in a workspace, on iOS and Android, in one place. It is in development; this page is for contributors.
Shape
- One source of logic. The app never calls a store itself. It runs the storebound CLI as a sidecar
(
storebound serve --stdio, compiled withbun tools/build-sidecars.ts) and asks it for everything. - App-centric. One app = one product with up to two platforms (iOS bundle id / App Store Connect app id, Android
package name), one
storebound.config.tsper app folder, many apps per workspace. Navigation: Portfolio -> App (Releases, Catalog, Prices, Listings, Store art, Health, Reviews, TestFlight, Growth, App info, Checks) plus the workspace screens (App folders, Accounts, Dependencies, Settings). - Release trains. A version (for example 2.3.0) spans both stores: iOS build and Android versionCode, per-store
state (TestFlight, review, live, phased release, rollout fraction).
portfolio.statusreturns per app the current train (highest version live on a store) and the next one (highest version above it anywhere). - Catalog parity. Products (and subscriptions, once the config has them) are defined once;
portfolio.statuscompares the config with both stores (ids missing on a store, ids on one store only).
Types of the protocol: cli/lib/desktop-protocol.ts; pure portfolio logic: cli/lib/portfolio.ts.
Safety model
- The sidecar's first request is
session.hellowith a 32-byte secret the Rust host makes per start and writes to the sidecar's stdin. Write methods are dry runs unless the call carries a confirm token: HMAC-SHA256 over the method, the canonical{ project, args }and a 60 s expiry, single use. The host mints one only after the owner pressed Apply in a native dialog that shows the summary and the dry run (write_with_confirm). - No method releases an approved version, publishes, cancels a submission, sends a build to external testers or
removes testers. Under
serve, the CLI's own terminal confirmations answer no, with one exception: the owner actions (owner.play_rolloutshare / halt / resume / complete,owner.ios_phasedon / pause / resume / complete,owner.reply_sendone approved draft,owner.testflight_testersadds the testers of.storebound/testers.csv,owner.prices_importandowner.prices_play_products/_play_subscriptions/_apple_subscriptionswith--allow-price-changes,owner.ppo_start/owner.ppo_stop, and sending for review:owner.apple_submit(apple submit --build n),owner.apple_resubmit(apple resubmit --build n [--resolve-iaps]),owner.apple_iap_submit(apple iap-submit [--with-version]) andowner.play_production(play release --version-code n --production completed | inProgress | draft)). They are desktop-only (never MCP tools), dry runs from the window like every write, and their real run needs the host token. Only then serve setsSTOREBOUND_OWNER_CONFIRMEDfor that one child, which answers the command's question, and adds the owner-only flags (--yesof apple submit / resubmit,--allow-price-changes);OWNER_ALLOWED_ARGSnames the one action each such flag belongs to and a dry run never carries one. Builds and versionCodes are picked by number in the window, never "the latest". The native dialog names the action on its button ("Yes: Submit for App Review"); for the submit actions it says that the app goes to review and may go live by itself once approved. Play's "Send changes for review" (Publishing overview) has no API: the Releases tab shows its console plan and a deep link instead.tests/desktop-d3.test.ts,tests/serve.test.tsandsrc-tauri/tests/d3_writes.rspin the token binding of every write the window offers and that the MCP server has none of these. - The webview gets no plugin API. It may call the app commands (each re-checks the window label and the page origin
in Rust) and listen to two events. Strict CSP, no remote content.
src-tauri/tests/security.rspins all of it. - Store keys (Play service account JSON, App Store Connect key id, issuer id and .p8) are picked in native dialogs,
stored in the OS keychain (
keyring), never shown again and passed to the sidecar per call in memory only.
Screens and data (D2)
- Portfolio: KPIs (apps, live versions, rolling out, in review, needs attention), one row per app with the version reaching users on each store and its place on the track (build, testing, review, live, rollout %), the next version, the last saved crash rate as a sparkline, catalog parity and the config check; then a "needs attention" list (problems first, then warnings, store read errors, rejected or halted releases, catalog drift, health near a threshold).
- App: Releases (release timeline, both stores on one track per version; iOS phased release day and share from
apple status --json), Catalog (parity matrix fromcatalog.diff: matches, differs, missing, not sold there, not compared; price gaps per region), Listings (project.listings: per language and store, fields against the limits, rendered screenshots), Health (health: daily user-perceived crash and ANR rate against the Play thresholds, worst devices, top issues, App Store metrics and signatures), Reviews (reviews.list+reviews.drafts: star distribution, filters, the local draft with its send checks, Approve viareviews.approve), TestFlight (apple.testflight_status), Checks (offlinescan: rejection risks with guideline, evidence file:line, fix and skill, owner-accepted ones marked; offlinecheck;validatewith fix hints). - First-run guide: what the app does, add or start an app folder (
project.init=storebound init --profile, confirmed in a native dialog), store keys into the keychain and which app uses which, local tools, first read. - Command palette (Cmd/Ctrl+K): apps, tabs, screens and actions. Cmd/Ctrl+1 Portfolio, +2-5 workspace screens, Cmd/Ctrl+Shift+[ ] app tabs, arrow keys in the tab list. Toasts show background jobs with progress and Cancel.
- Unknown, never false. Store reads run on demand (they use the account's API quota) and are saved in app data
(
cache.sqlite, results only, never keys); a screen opens with the saved read and its time ("Last read 3 h ago, saved copy"), a failed read keeps the previous one and says so, and nothing that was not read is shown as fine. Settings clears the saved reads. - Jobs. The window tags a call with a job id; the host maps sidecar request ids to it, so progress lines reach the
right toast, and
job_cancelsends$/cancelRequest(or stops a download). Live portfolio reads run up to four apps at a time with their own accounts and report n / total. - i18n: English and Turkish (
src/i18n), Settings picks the language (default: the system's);tools/i18n-check.ts(CI) checks keys, empty messages and placeholders. Theme: text tokens keep 7:1 contrast in both themes (tests/theme.test.ts), motion at most 200 ms and off with reduced motion. - Charts, icons, art: libraries only, no hand-drawn SVG (owner rule, guarded by
tests/no-handmade-svg.test.ts). Charts use visx (@visx/*, MIT;src/ui/charts.tsx,src/ui/LineChart.tsx): Growth's indexed lines with the 100 baseline, listing-change markers and a crosshair tooltip with the real values; Health's crash / ANR series against the bad behavior threshold (crosshair + tooltip, arrow keys when focused); the portfolio's compact sparklines; the rollout / phased release bar. Widths follow the container (useParentSize), colours are the theme tokens, nothing animates. Icons are Phosphor (@phosphor-icons/react, MIT), one per-icon import each (@phosphor-icons/react/dist/csr/<Name>, so only the icons used are bundled);src/ui/icons.tsxmaps the app's icon names to them. One weight, "regular"; "fill" only marks state (the active nav item, notices, rating stars). Store marks are the stores' own logos from simple-icons (CC0 data), vendored unchanged intosrc/ui/store-marks/bybun tools/site-icons.tsand shown as a CSS mask in the neutral text colour, always next to the store's name. They identify the store only: storebound is not affiliated with Apple or Google, and the marks are never styled as its own branding. App Store is a trademark of Apple Inc.; Google Play is a trademark of Google LLC. Empty states (src/ui/illustrations.tsx) are a Phosphor icon on a token-coloured tile with an optional bold badge icon, no artwork. The fixtures' app icons and screenshot stand-ins are PNGs frombun tools/fixture-art.ts(Phosphor "fill" icons on CSS gradients, photographed by Chromium). The brand mark (src/ui/brand.tsx) is a PNG (src/ui/logo-mark.png, fromtools/brand-assets.py, seebrand/README.md): no inline SVG at all. Bundle cost: about +97 kB minified, +31 kB gzip for visx (d3-scale / d3-shape and @visx/text for the axes). The Phosphor icons cost about 168 kB minified / 41 kB gzip more than the lucide set they replaced (each per-icon module carries all six weights; 864 kB / 247 kB gzip for the whole webview script).
Studios, accounts and store-only apps (D3)
Accounts and studios
- A studio is a name, a colour and an optional logo. It groups any number of store accounts:
- a Play service account, which can be invited to several Play developer accounts and then sees all their apps;
- an App Store Connect team key, one per team.
- Keys stay in the keychain. The state file keeps labels and studio ids only.
storebound apps checkgives each account's health, with the fix in words:- ok;
- no-apps;
- permission (Play: "invite in Play Console > Users and permissions with ...");
- revoked;
- api-disabled.
storebound apps listreads the apps an account can see:- App Store Connect:
GET /v1/apps, the newest version, and an icon from the newest build'siconAssetToken, fetched by the CLI and returned as a data URL; - Play: the Reporting API
apps:search.
- App Store Connect:
- Serve exposes both as
apps.listandaccount.check(kindaccount, one account's key per call, run in an empty temp folder). The host caches app lists underacct:<id>.
One card per app
src/lib/identity.ts builds one card per app, pure and tested:
-
A folder takes the store apps its config names. Order: an explicit link, then the App Store app id, then the bundle id or package name. The folder's mapped accounts win.
-
The remaining store apps pair iOS + Android, in this order:
- manual pairs;
- the same id;
- a close id (
.ios/.androidsuffixes, case, punctuation); - the same name.
Unpairs block automatic pairs.
-
What is left is a single-store app.
Overrides (pairs, unpairs, links, studio moves, the ignore list of apps, folders and whole accounts) live in the
host state file (overrides_set, every key checked in workspace.rs).
Store-only apps
- A store-only app has no folder. It is read through a store target:
store:ios:<account>:<app id>:<bundle>;android:<account>:<package>. - The host checks the target and the account kinds, and passes
virtual: { name, apple, play }. The sidecar writes a minimalstorebound.config.json(store ids, placeholder links) into its temp folder. - Only
apple.status,play.status,health,reviews.list,apple.testflight_statusandapple.ppo_statuswork this way (tabs Releases, Health, Reviews, TestFlight, Growth), andportfolio.statustakes store-only entries. - "Link a folder", "Start an app folder here" or "Import from fastlane" turn it into a full app.
Settings
- Studios and accounts.
- Pairing and the ignore list.
- Store reads: on launch, every 15 min to 12 h (off by default), apps at a time 1-8, icons.
- Offline cache: size, keep 7 / 30 / 90 / 365 days or always, clear.
- Language, theme and motion.
- Notifications, after a read, never on the first one: review / release state changes, new 1-2 star reviews, crash or ANR over the Play threshold.
- Dependencies and the CLI sidecar.
- Privacy, with a button that reveals the data folder.
- Shortcuts.
- Updates: channel (stable / beta), check and install (D5).
- Reset: native confirmation, then the keychain entries and every saved state are removed.
Writes in the window
Each write is a dry run, then the native dialog, then the real run, with one progress toast and Cancel:
- Listings tab: Play listing text, App Store version and text, Play graphics, App Store screenshots and previews; local render jobs (screenshots, previews, icons, gallery).
- Catalog tab: Play products and subscriptions, App Store IAPs and subscriptions, price export; price changes of
live products (
owner.prices_play_products,owner.prices_play_subscriptions,owner.prices_apple_subscriptions, owner actions: a normal sync holds them back and exits 3, shown as "Waiting for your decision"). - Prices tab: the price sheet compared with the stores and applied (
owner.prices_import, an owner action). - App info tab: Play Data safety upload (the deletion question as the CSV answers it and where the answer comes
from), App Store app info, export compliance, App Review details; "Console steps without an API": every form of
console plan(App Store app record, age rating, App Privacy; Play Create app, store settings category, every App content card, content rating, target audience, production countries, production release) with its plan panel (start page, steps, answer sheet, never-press buttons, Copy plan for an AI agent) and the read-only check (console.verify, page text pasted or the API), plus "Fill in console" where the scripted path exists. - Growth tab: analytics snapshot and report request, PPO create and treatment upload; PPO start / stop
(
owner.ppo_start,owner.ppo_stop, owner actions). - Store art tab: render, review page.
- Releases tab: rollout and phased release owner actions; Send for review: App Store submission state from
apple status(waiting / in review, UNRESOLVED_ISSUES with its items, empty drafts labelled harmless), the uploaded builds by number, Submit for review / Resubmit (with the rejected items to mark resolved) / in-app purchases; Google Play versionCodes by number, Send to production (everyone, staged, or a draft for a never-published app), the managed-publishing warning and the console plans for production countries and "Send changes for review". Each button shows the plan (dry run) in the window first (ui/OwnerAction.tsx), then the native dialog; progress lines of the real run stream into the panel. - TestFlight tab: What to Test, internal groups, testers, "Create missing groups" (
apple testflight groups sync, dry run first). - Dependencies screen: the Android SDK as
play buildfinds it (system.toolsandroidSdk). - Job toasts: the command's progress lines; upload lines ("312/890 MB (35%), part 4/12") drive the bar, and the last 40 lines are in the toast's log.
- Reviews tab: reply editor (
reviews.reply), approve, send (owner action).
The same model in the terminal
The CLI workspace (storebound.workspace.json, docs/studios.md "Many apps and studios") holds the same studios, accounts
(credential profile names instead of keychain entries), folders and overrides. The pairing rules
(cli/lib/identity.ts) and the release lanes (cli/lib/train.ts) are one implementation for both; the window
re-exports them (src/lib/identity.ts, src/lib/model.ts). storebound ws desktop import reads state.json into a
workspace file (the keychain is never read; the profiles to create are listed); storebound ws desktop export --merge --apply adds a workspace to state.json while the app is closed (backup kept, nothing removed, new accounts get
their keys in the Accounts screen). Project ids in an exported state are stable per folder path (16 hex).
Fill in console
Some store forms have no API: the App Store app record (New App), the age rating, App Privacy, Play's Create app
form, and the Play content rating and target audience questionnaires. The App info tab has a Fill in console
button for each (App Privacy card, App info card for the age rating, a "Play Console forms" card, and a notice when the config has no
App Store app id yet). It runs storebound console fill <form> through the desktop-only method console.fill (a
write, never an MCP tool):
- The click runs the dry run (
console fill <form> --dry --json): nothing opens. A panel lists the answers from the config; the ones only the owner knows are marked "You answer this". It shows the note: "Console pages change. This is AI-assisted: review every field in the browser and press the final button (Publish, Submit, Save or Create) yourself. storebound moves through the pages for you and never submits or publishes; App Privacy answers stay a draft until you press Publish." - Open the browser goes through
write_with_confirm: the host repeats the dry run and shows the answers and the note in a native dialog ("Open the browser" / Cancel). Only that button mints the confirm token. - The real run opens a visible Chromium with its own profile. The owner signs in (storebound never types passwords);
storebound fills the fields, highlights the uncertain ones, clicks the navigation (Next, App Privacy's Get Started /
Edit / data types and its draft Save) and never clicks Submit, Publish, Create or a final Save. The
run lasts until the owner closes the browser: no timeout in serve, the runner or the host. Progress lines show in
the panel; Cancel: close the browser sends
$/cancelRequest, which stops the command and its browser.
The browser is the same as in the terminal (cli/console/session.ts): a Chrome running with remote debugging, else
storebound's own profile (the owner signs in there once; the native dialog says so) in the desktop's Chrome for
Testing (STOREBOUND_CHROME), started by storebound without automation switches and attached with the bundled
playwright-core. The desktop never copies the owner's Chrome cookies (Google binds them to the device; see the
console-autofill skill); --use-chrome-session is a terminal-only opt-in. The tool's fresh_profile: true passes
--fresh-profile (skip attach). storebound opens the form itself after sign-in (navigation clicks only).
The own profile is one per store (every studio shares the one login) and keeps the sign-in between runs: launched with
session restore, closed gracefully on Cancel. With one login in several Play developer accounts or App Store Connect
teams, the run finds the account that has the app and remembers it in ~/.storebound/console-accounts.json
({ version, play: { <package>: { developerId, account, at } }, apple: { <bundle id>: { team, teamId?, at } } }, ids
and account names only), the same file the desktop reads. A workspace account's developerId (play) / teamId
(apple) is passed as STOREBOUND_PLAY_DEVELOPER_ID / STOREBOUND_APPLE_TEAM_ID.
Every CLI area in the window
| CLI | where in the window |
|---|---|
prices export --json (new, read-only) |
Prices: product x territory matrix (both stores, Apple manual / equalized), per-territory table with gaps |
prices export |
Prices: "Write store/prices.csv" (confirm flow); "Export prices CSV" saves the same sheet anywhere |
prices import --dry --json / prices import |
Prices: compare store/prices.csv or a picked CSV (price_sheet_pick), diff table, Apply = owner.prices_import |
inspect --declarations --json (new) |
App info: Data safety, App Privacy suggestions, app info, encryption, review details, "ask the owner" items |
play data-safety |
App info: Upload (keeps .storebound/data-safety.uploaded.csv, so the tab shows changes since; Play cannot read the form back) |
apple appinfo, apple encryption, apple review-details |
App info: Push buttons (confirm flow) |
console fill <form> --dry --json / console fill <form> --json (desktop-only console.fill) |
App info: "Fill in console" (answers panel, native dialog, visible browser the owner saves in) |
apple ppo status / create / upload / start / stop |
Growth: tests table, create, upload, Start / Stop owner actions |
apple status --json (review submissions, UNRESOLVED_ISSUES items, empty drafts, builds) |
Releases: Send for review, App Store card |
apple submit --build n, apple resubmit --build n [--resolve-iaps], apple iap-submit [--with-version] |
Releases: Submit for review / Resubmit / Submit in-app purchases (owner actions) |
play status --json, play release --version-code n --production ... |
Releases: Google Play card, Send to production (owner action) |
console plan <form> --json, console verify <form> --json |
App info: Console steps (every form); Releases: production countries, Send changes for review |
apple testflight groups sync |
TestFlight: Create missing groups |
play products / play subscriptions / apple subscriptions --allow-price-changes |
Catalog: Price changes (owner action) |
doctor Android SDK |
Dependencies: Android SDK card |
| Play store listing experiments | no API: Growth links Play Console and the variant renders |
analytics compare --json, snapshot, setup |
Growth: chart (reach, installs, conversion, listing changes as markers), before / after table, snapshot and setup buttons |
shots gallery --json, inspect --render (new) |
Store art: grid per variant / language / size, issues, lightbox; images load lazily one at a time as data: URLs (never cached) |
shots render, shots gallery |
Store art: Render (language, variant), Write review page, Open review page / folder (project_reveal) |
inspect --listing-text --json (new) |
Listings export (Markdown) |
import fastlane |
First-run guide, App folders, store-only app banner (project_import_fastlane: plan in a native dialog first) |
Exports. Every screen registers what it can save (src/ui/Exports.tsx): the "Export" menu at the top right of
the top bar and the "Export" group of the command palette. Files: prices CSV (the prices import format), listings
(md), reviews CSV, health report (md), catalog parity (md), store readiness report (md), store declarations (md),
Data safety CSV, analytics CSV, portfolio CSV. export_save writes only through a native save dialog (Downloads
first), only csv / md / json / txt, sanitized names, at most 16 MB; the reviews export shows a native personal-data
note first.
UX audit (full CLI coverage)
- Two clicks. From an app page every action is one tab plus one button: prices, Data safety, app info, review details, PPO, analytics, renders. The command palette jumps to Prices and Store art and runs every export.
- One place for exports. Before, the only export was a write button in Catalog. Now each screen's exports sit in one menu at the top right (the same spot on every screen) and in the palette; disabled items say why.
- Buttons. Card head actions go from secondary (ghost: open, show) to the primary action at the right end; owner actions are named on the button and in their native dialog. App folders: Import from fastlane, Start an app folder, Add app folder (primary) in the page header, the same three in the empty state; each folder row has Open and Show.
- Empty states say what to do. Prices not read (Read prices), no products (where to add them), no snapshots (what a snapshot is, the bucket for Play), no PPO tests (create from a variant, or add variants), no renders (Render), no Data safety answers (which skill), store-only app (Link a folder / Start / Import).
- Errors say how to fix.
fixHint(src/lib/views.ts) maps known CLI errors (keys, permissions, config values, ffmpeg, Chrome, renders, snapshots, bucket, editable version, fastlane folder, price sheet, price points, network, history) to the step that fixes them, shown under read errors and in failure toasts. - Ask, never guess. App info opens with the questions only the owner can answer (age rating, App Privacy linked / tracking, encryption, sign-in, contact), highlighted in amber, each with how to answer.
- First run. The guide now starts with the studio (pick or create, with a colour), so the keys added two steps later land in it; the app step offers add / start / import from fastlane side by side.
- Tabs. Eleven app tabs fit 1440 px; narrower windows scroll the tab bar sideways instead of wrapping.
R&D on a fastlane importer and a RevenueCat adapter: docs/research/fastlane-revenuecat.md.
Dependencies screen
storebound needs no local tool for store work. Screenshots, app previews, icons and captures use:
| tool | used for | how the app gets it |
|---|---|---|
| ffmpeg, ffprobe | icons, renders, app previews, frame crops | download into the app data folder (planned, see below) |
| Chrome / Chrome for Testing | screenshot renders, captions, web captures | system Chrome, or Chrome for Testing downloaded into the app data folder (planned) |
| Xcode command line tools, simctl | iOS builds, Simulator captures | link to Apple only |
| adb | Android emulator / device captures | link to the Android platform tools only |
| git | release notes, analytics compare | link only |
system.tools reports each with its path, version and source (app or system). The host passes app-managed
copies to the sidecar: STOREBOUND_TOOL_PATH (app data tools/bin, put in front of PATH for every command) and
STOREBOUND_CHROME (app data tools/chrome/..., used as Playwright's executablePath). Nothing is installed
system-wide and nothing is bundled in the installer.
storebound doctor prints the same detection (plus playwright) with the install commands for the OS.
Download sources (one click on the Dependencies screen or in the first-run guide; src-tauri/src/downloads.rs):
- ffmpeg, macOS (arm64, x64): release builds from ffmpeg.martin-riedl.de (static, notarized zip per tool).
- ffmpeg, Linux (x64, arm64) and Windows (x64):
ffmpeg-n<major>-latest-<os>-gplrelease builds from github.com/BtbN/FFmpeg-Builds. - Verification: the app ships a manifest (
src-tauri/tools-manifest.json, written bybun tools/tools-manifest.tsbefore each desktop release,--checktests the URLs) with the exact URL, size and SHA-256 of each archive; a download whose hash differs is deleted. Every pinned hash has two independent sources: macOS archives are downloaded and hashed by the tool and must equal the publisher's.sha256; BtbN archives (pinned autobuild tag) use the digest GitHub computes for the release asset, which must equal the publisher'schecksums.sha256. Downloads and their redirects may only use HTTPS and the hosts indownloads::DOWNLOAD_HOSTS. Everything is unpacked and checked in a staging folder; only a verified result replaces the installed copy, and the sidecar restarts so it sees the new tool. - License: these ffmpeg builds are GPL (v3 for the BtbN
gplvariants). The app downloads them only on the owner's click, from the original publisher, shows the license and the source link next to the button, and never redistributes them, so the app itself stays MIT. - Chrome for Testing: Google's official JSON endpoints
(googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json, Stable channel), archives
from storage.googleapis.com over HTTPS. Those endpoints publish no hashes, so the app checks the code signature after
unpacking (macOS
codesign --verifywith Google's team, Windows Authenticode); on Linux it checks the version banner. Chrome for Testing is under Google's terms for Chrome / Chromium (BSD-style Chromium license plus third-party notices). - Progress and cancel go through the same progress events as sidecar jobs; a cancelled download removes its temporary file.
Release, updates and the terminal bridge (D5)
- Releases: desktop-release.md (version authority, privacy gate, macOS signing and notarization, Forgejo builds for Linux and Windows, storebound.dev hosting, publish).
- Updates: Settings > Updates checks storebound.dev (stable
/desktop/latest.jsonor beta/desktop/beta/latest.json) only when asked; Install shows a native dialog, then the updater downloads the bundle, checks its minisign signature againstplugins.updater.pubkeyand restarts (update_check,update_install). storebound://open?path=<absolute folder>(sent bystorebound desktopin an app folder): the host accepts one plain absolute path (policy::deep_link_open_path), selects that app folder when it is in the list, otherwise asks in a native dialog and adds it (deep-link-folderevent). A folder without a storebound config gets a message, nothing else.storebound-desktop --self-test: starts the bundled sidecar, checks thesession.hellohandshake, prints one JSON line and exits, before Tauri starts (no window). The release runs it on the exact artifacts.
Never a blank window (D6)
- Frozen prototype.
freezePrototype: truemakes Tauri runObject.freeze(Object.prototype)before any page script. Dependencies that assignx.constructor = ...orx.toString = ...on a plain object (d3-color, d3-format under the visx charts) then throw while the bundle loads ("Cannot assign to read only property"; WebKit: "Attempted to assign to readonly property"), and the window shows only its background colour, before React and before the sidecar starts.apps/desktop/build/freeze-safe.ts(a Vite plugin) rewrites those assignments in dependencies into a helper that defines the property when the frozen prototype would refuse it. - Saved state versions.
state.jsoncarriesversion(2). Files without it are format 1 (D1 to D3: projects and accounts, later studios, overrides and settings) and are migrated once (store::migrate), the original kept asstate.json.v1.bak. An unreadable file is kept asstate.json.unreadable-<time>and the app starts empty; entries that do not parse are dropped one by one (named in host.log), settings missing a field keep the rest. - Folders that are gone (moved, renamed, deleted, old worktrees):
projects_listmarks themmissing; the app shows "Folder not found" with Locate… (project_locate: keeps the id, accounts, links and saved reads) and Remove from list. Portfolio reads skip them. - Error screens. A React error boundary around the whole window and one per screen and app tab show the error,
Copy details, Reload and Reset saved state (keeps a backup:
state_resetmovesstate.jsontostate.json.reset-<time>.bakafter a native confirmation; keys stay in the keychain).public/boot.jsruns before the bundle and shows the same panel in plain DOM when the bundle fails or the window stays empty. - Logs in
<app data>/logs:host.log(start, saved state and migration, sidecar start / stop with its last stderr lines, panics) andwebview.log(window.onerror, unhandled rejections, boundaries, throughlog_webview, at most 60 entries a minute). Rotated at 512 KB (one.1.logkept); home folder shown as~, keys, tokens, passwords and e-mail addresses replaced. - Boot check (
bun tools/desktop-boot-check.ts, inbun run ciand the macOS release): the builtdist/in headless Chromium with the release CSP and the frozen prototype, a mocked host answering from the saved state fixtures (apps/desktop/tests/fixtures/state: empty, D1 with folders gone, D3 full; the host answers are written by the Rust migration itself,src-tauri/tests/state.rs) and the real sidecar for reads. It opens every screen and app and fails on any uncaught or console error, an error panel or an empty#root; a last case removes the bundle and expects the boot.js panel. One browser (killed at the end), a page per case, the cases in parallel against the one build, and no fixed sleeps: each step waits for its element, its text or a quiet window (no host call in flight, no DOM change).
Develop
bun install
bun tools/build-sidecars.ts --host # apps/desktop/src-tauri/binaries/storebound-<target triple>
cd apps/desktop && bunx tauri dev # Vite on 127.0.0.1:1420 + the Rust host
cd apps/desktop/src-tauri && cargo test && cargo clippy --all-targets -- -D warnings
Dev-only switches (ignored in release builds): VITE_START=accounts|app:catalog|app:health:2|welcome:3|palette|...
opens a screen first, VITE_THEME=light|dark and VITE_LANG=en|tr force theme and language, VITE_AUTORUN=gallery
starts the confirm flow on the first app (the same names work as ?start=...&theme=... query parameters on the Vite
server). STOREBOUND_DEV_PROJECTS=<dir>:<dir> (debug builds) seeds the app folder list.
Fixture mode (dev builds only): VITE_FIXTURES=1 answers every app command from src/dev/fixtures.ts (made-up
store data around the two demo apps; =empty for a first run, =cold for nothing read yet). The window shows a
"DEV FIXTURES" badge; release bundles do not contain it (tests/webview.test.ts checks dist/). Screenshots of
every screen in both themes: VITE_FIXTURES=1 bunx vite --port 1431 & then
bun scripts/screens.ts --base http://127.0.0.1:1431 --out <dir> (playwright with the installed Chrome).
Gotchas kept from earlier Tauri apps: Cmd+Q quits and the Window menu keeps an entry for the hidden window; platform
configs (tauri.<os>.conf.json) must not set app.windows (Tauri replaces the whole array); Linux sets
WEBKIT_DISABLE_DMABUF_RENDERER=1 unless the user set it; a bun build --compile binary needs an ad-hoc re-sign on
macOS or the kernel kills it on start.