Skills DirectorySearch all skills ↗

AI agent skill · by @EpicenterHQ

Platform Seams

Apply Epicenter’s `#platform/*` build-time seam across browser and host targets. Use when adding or changing a seam, build condition, typecheck leaf, or deciding whether code belongs behind one.

Archived 2026-10-23 · Source updated 2026-10-12. Check the original repository for the latest version.

SKILL.md instructions

# 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. Applications acquire independent stores through
`openLocal` and `openPersonal` from `@epicenter/app/open`. Resource constructors
select browser or native implementations with `isTauri()` (ADR-0403).
`StoreRuntime` injection controls store storage and admission; it is not an
application-wide runtime. `defineStore` from the root is platform-free and
carries no runtime or AI override.

App-level seams select auth and product capabilities. They do not choose store
persistence. Keep resource composition at the product boundary; do not restore
Whispering's removed `#platform/runtime` seam. For constructors and ownership,
read the [resource contracts](../../../packages/app/README.md).

## Whispering is not a seam app

Whispering is a standalone desktop app whose data is a folder of files. It has
no `#platform/*` map and no build condition. Its one boundary is the workspace
type in `apps/whispering/src/lib/workspace/`, chosen at runtime by whether the
desktop shell injected a bridge, and its native code runs in a separate folder
host process. The "storage is not a seam" rule above is about store-backed apps
and does not describe it. See `apps/whispering/ARCHITECTURE.md`.

## Declaring one

**1. Map the specifier in `package.json` "imports".** One entry per condition,
plus a `default`:

```jsonc
"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:**

```ts
import { auth } from '#platform/auth';
```

**3. Activate the condition in `vite.config.ts`:**

```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:

```ts
// 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.

**Package selection and app seams.** `@epicenter/app` selects its default runtime
and clipboard inside the package with `isTauri()` (ADR-0403). It requires no
consumer build condition. Keep app-level conditions where they select auth or
product UI; removing the package map does not remove those app-owned choices.

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 an app's `#platform/*` call site. Import the
  bare specifier and let the build select the leaf.
- Detecting the host at runtime in an app seam. The build already answered.
  `@epicenter/app` is the explicit exception: ADR-0403 selects its runtime and
  clipboard with `isTauri()`, under the supported Epicenter-host-only Tauri model.
- Using `satisfies` on a leaf instead of a `: Contract` annotation.
- Importing a `.tauri.ts`-only symbol through `#platform/*`. It resolves to the
  browser leaf off Tauri; import it directly from the `.tauri` module inside
  another `.tauri.ts` file.
- Reintroducing `resolve.extensions` suffixes or tsconfig `moduleSuffixes`.
- Dropping `...defaultClientConditions` from the Tauri `conditions` array.
- Adding a condition leaf with no `tsconfig.<condition>.json`, which means it is
  never typechecked.
- A `#platform/*` seam over identity. Whether a person is signed in is runtime
  state, not a build condition, so the gate is an `{#if}` in the boot node and
  never a leaf. Every app renders a sign-in screen when signed out (ADR-0342,
  rejected: sign-in IS a door), and that is one branch in one file rather than a
  seam.

Related skills