Platform seams
SkillWeb & browsingGuides your agent in applying Epicenter's #platform/* build-time seams across browser, Tauri, and host targets.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Platform seams skill
About this capability
Apply Epicenter’s `#platform/*` build-time seam across browser, Tauri, and host targets. Use when adding or changing a seam, build condition, typecheck leaf, or deciding whether code belongs behind one.
What this skill tells your AI
The instructions your AI receives, as published by epicenterhq/epicenter in .agents/skills/platform-seams/SKILL.md and read by ahel’s review.
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.
Signals
- GitHub stars
- 5k
- Forks
- 378
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
platform-seams- Source
- github.com/epicenterhq/epicenter