Surface × Cordis / API sketch / 05 September 2026

One app.
Plugins all the way up.

surface-cordis-app composes the runtimes.
Your bundle chooses the shell, transports and domain plugins.

Proposed usage, not runnable code. The new package, helpers and lifecycle promises below are API sketches. The defineSurface example uses the existing API. App-specific store and view implementations are omitted.

01

Start the application

app.ts
import { defineApp } from "@kolu/surface-cordis-app"
import { web, mcp } from "@kolu/surface-cordis-app/plugins"
import { workspaceShell, inspector } from "@example/workspace-shell"
import { notes } from "./plugins/notes"

const domain = [notes]

export const app = defineApp({
  profiles: {
    web:      [...domain, workspaceShell, inspector, web({ port: 3000 }), mcp()],
    headless: [...domain, mcp()],
  },
})

await app.start({ profile: "web" })

The app host composes plugins. The workspace shell is a plugin selected by the bundle.

02

Keep the ordinary Surface contract

plugins/notes/wire.ts
import { Schema } from "effect"
import { defineSurface } from "@kolu/surface/define"

export const notesSurface = defineSurface({
  cells: {
    title: { schema: Schema.String, default: "Untitled" },
  },
})

This defineSurface shape already exists. Real notes would add collections and procedures here.

03

Declare the plugin’s separate entry points

plugins/notes/index.ts
import { definePlugin } from "@kolu/surface-cordis-app"
import { notesSurface } from "./wire"

export const notes = definePlugin({
  id: "notes",
  surface: notesSurface,

  server:  () => import("./server"),
  browser: () => import("./browser"),
})

Proposed build contract: the host resolves each entry into its own server/browser module graph. Headless never loads the browser entry.

04

Supply the server implementation

plugins/notes/server.ts
import { defineServer } from "@kolu/surface-cordis-app/server"
import { makeNotesDeps } from "./store"

export default defineServer({
  // Effect<ImplementSurfaceDeps<typeof notesSurface>, Error, Scope>
  setup: makeNotesDeps,

  expose: {
    browser: { title: "resource" },
    mcp:     { title: "resource" },
  },
})

// Host: initialize → mount Surface → publish availability.
// Unload: withdraw → settle owned work → release the scope.

makeNotesDeps is the application’s Effect that acquires its store and returns Surface dependencies. The host owns the mount, exposure and teardown coordination.

05

Depend on a shell only where you use it

plugins/notes/browser.tsx
import { defineBrowser } from "@kolu/surface-cordis-app/browser"
import { Workspace } from "@example/workspace-shell/contracts"
import { NotesView } from "./NotesView"

export default defineBrowser({
  needs: { workspace: Workspace },

  mount({ client, workspace }) {
    // client is the plugin’s ready Surface client.
    return workspace.panel({
      id: "notes",
      title: "Notes",
      render: () => <NotesView client={client} />,
    })
  },
})

// panel() returns an owned disposer.
// No Workspace provider → this browser component waits.
// Notes’ server component has no Workspace dependency.

Workspace and panel() belong to the shell plugin. The app framework only understands dependencies and owned contributions.

Inside the view: existing Surface usage
NotesView.tsx · excerpt
// Inside NotesView: ordinary Surface hooks.
const title = client.cells.title.use({
  authority: "server",
  onError: error => console.error("notes.title", error),
})

return <h1>{title.value()}</h1>
06

The shell is an ordinary provider

workspace-shell/browser.tsx
import { defineBrowser } from "@kolu/surface-cordis-app/browser"
import { Workspace } from "./contracts"
import { mountWorkspace } from "./ui"

export default defineBrowser({
  mount({ root, provide, own }) {
    const workspace = mountWorkspace(root)
    own(workspace.dispose)

    provide(Workspace, {
      panel: workspace.registerPanel,
    })
  },
})

// The bridge withdraws Workspace and joins dependent cleanup
// before disposing the workspace. Cordis owns dependency resolution.

The shell implements its layout and registration service. Surface has no panel or slot API. Settings and inspector UIs are consumers of shell services too.

07

Changing the app is one operation

runtime.ts
await app.plugins.disable("notes")
// Its browser contribution, wire availability and MCP catalog retract.
// Unrelated plugins and their local UI state keep running.

await app.plugins.enable("notes")
// Fresh server activation; clients follow the new mount.
// Browser contribution activates after its dependencies are ready.

await app.plugins.disable("workspace-shell")
// Dependent UI contributions unload. Notes’ server stays running.

await app.stop()
// Join plugin cleanup, then close the host.

These methods express a proposed settled-lifecycle contract. Failure is surfaced; a failed or hanging finalizer is never reported as successful cleanup.

08 / Implementation beneath the API

What we actually have to build.

Usage aboveFramework work underneath
defineApp / definePluginExtract Olai’s app composition and split module loading. Reuse Cordis configuration and dependency resolution.
setup / provide / ownIntegrate the Effect–Cordis bridge. Make readiness, withdrawal, dependent cleanup and mount-failure ownership explicit.
A ready clientFollow versioned composition, mount replacement and connection-policy changes without rebuilding surviving UI.
expose.mcpMake catalog updates and dispatch follow the same committed exposure. Transport plugins use the existing Surface adapters.
workspace.panelShell-plugin code. Its contracts, layout and rendering stay outside Surface and the permanent app host.

First proof: extract one real Olai capability into this shape. Run it with a shell, then headless. Disable and re-enable it without rebuilding unrelated UI. Let that slice determine the API.

Root-level contributions and stronger independent-plugin contract checks remain candidates; neither is assumed necessary for this first slice. The permanent host must stay free of shell-specific contracts.

Evidence behind the sketch

Already merged: live mounts #2223 · live accepts #2225 · stable clients #2228 · live header policy #2229.

Extraction seed: Olai’s Effect–Cordis bridge. Composition gap: Olai’s browser coordination. Theory: Cordis paper. Disposing registrations does not undo emitted writes or messages.

Notes stay in this open tab. Export before closing or reloading.