Platform seams
Replaces workspace-app-composition, whose subject (defineWorkspace, the
workspace singleton, environment factories, the daemon) was deleted with the
workspace plane. The seam below is the part that survived.
Storage is not a seam
Every build opens its own store. A host serves bundles and brokers
credentials and owns no application data (ADR-0226), so there is no build where
data lives somewhere else, and a #platform/* seam for storage is the thing to
delete rather than to route. Honeycrisp calls openBrowserStore in every build
including the Tauri one, and apps/honeycrisp/src/lib/application-platform.ts
states that as a refusal with its reasons.
What is left behind a seam is how a build gets a bearer and which deployment it
talks to, plus native capability. Honeycrisp declares exactly two:
#platform/auth and #platform/instance.
Declaring one
1. Map the specifier in package.json "imports". One entry per condition,
plus a default:
"imports": {
"#platform/auth": {
"epicenter-host": "./src/lib/platform/auth.epicenter-host.ts",
"tauri": "./src/lib/platform/auth.tauri.ts",
"default": "./src/lib/platform/auth.browser.ts"
}
}
Condition order in the map is match order.
2. Import the bare specifier, with no branch at the call site:
import { auth } from '#platform/auth';
3. Activate the condition in vite.config.ts:
resolve: {
// Custom conditions REPLACE Vite's defaults, so the
// ...defaultClientConditions spread is LOAD-BEARING: drop it and all
// dependency resolution breaks.
...(isEpicenterSurface && {
conditions: ['tauri', ...defaultClientConditions],
}),
},
4. Typecheck every leaf. Bundler moduleResolution reads the imports
field and lands on default for the editor, so the browser leaf is free. Every
other condition needs its own tsconfig.<condition>.json setting
compilerOptions.customConditions, run from typecheck. Without one, those
leaves are never checked at all.
5. Give the seam a contract. types.ts declares it; each leaf annotates
against it:
// platform/types.ts
export type PlatformAuth = { /* ... */ };
// platform/auth.browser.ts
export const auth: PlatformAuth | undefined = browserAuth;
Use export const x: Contract = ..., not satisfies. satisfies leaks the
concrete type and breaks the lockstep that keeps every leaf the same shape.
The two conditions answer different questions (ADR-0190)
tauri means this build runs in a Tauri WebView, so a leaf may call a native
command. epicenter-host means the desktop Epicenter host serves this build,
so a leaf may reach the host for a credential, a deployment choice, or an asset
base.
They used to be conflated because epicenter-host also meant the host owned the
build's replica. ADR-0226 removed that, so the condition is now about brokered
credentials and nothing about data.
A build that owns its own storage uses neither host condition, whether a browser or a bundle serves it: a WebView is a storage partition and origin pair like any other (ADR-0177). Every build owns its own storage, so this is now always true.
A dropped leaf fails nothing
This is the one hazard worth remembering. Removing an epicenter-host key from
a seam breaks no build: resolution falls back silently to default, and the
hosted build quietly runs the browser leaf. apps/honeycrisp/src/lib/platform-selection.test.ts
reads the declarations and names the broken seam, and
apps/epicenter/scripts/build-applications.test.ts runs the real build and reads
the emitted bytes.
Why not suffixes
The old mechanism put .browser.ts / .tauri.ts ahead of .ts in Vite
resolve.extensions, mirrored by tsconfig moduleSuffixes. That was global:
every bare import was magic, which is how a bare ./fuji once collided with a
fuji.browser.ts. #platform/* is scoped to those specifiers only, so the rest
of the import graph stays ordinary.
Anti-patterns
- Adding a
#platform/*seam for storage, a replica, or a database. Every build opens its own store; that seam is what ADR-0226 refused. - Branching on the platform at a
#platform/*call site. Import the bare specifier and let the build select the leaf. - Detecting the host at runtime. The build already answered.
- Using
satisfieson a leaf instead of a: Contractannotation. - Importing a
.tauri.ts-only symbol through#platform/*. It resolves to the browser leaf off Tauri; import it directly from the.taurimodule inside another.tauri.tsfile. - Reintroducing
resolve.extensionssuffixes or tsconfigmoduleSuffixes. - Dropping
...defaultClientConditionsfrom the Tauriconditionsarray. - Adding a condition leaf with no
tsconfig.<condition>.json, which means it is never typechecked. - Gating a route or the app shell on identity: no
(signed-in)route groups, no signed-out screen, no redirect-to-sign-in. Sign-in is an enhancement (ADR-0088), and signed-in-only features get small inline affordances.