Platform seams

SkillWeb & browsing

Guides 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.

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 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.
  • 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