# The Package Contract: Core, UI, Views

> Three layers with iron boundaries. packages/core exports raw .ts/.tsx (no build step), contains every Zustand store and the API client, and forbids react-dom, localStorage, and process.env. packages/ui contains only pure Base UI + shadcn components and may never import @multica/core. packages/views contains all business screens and may never import next/* or react-router-dom; it receives routing via NavigationAdapter injected by the platform layer. Each consuming app supplies its own thin adapter (web/platform/navigation.tsx, desktop/.../navigation.tsx) and wraps its root with CoreProvider. This is why the identical page component works in both apps and survives HMR.

- Repository: multica-ai/multica
- GitHub: https://github.com/multica-ai/multica
- Human wiki: https://grok-wiki.com/public/wiki/multica-ai-multica-fd1dd916d3bf
- Complete Markdown: https://grok-wiki.com/public/wiki/multica-ai-multica-fd1dd916d3bf/llms-full.txt

## Source Files

- `packages/core/platform/index.ts`
- `packages/core/platform/core-provider.tsx`
- `apps/web/platform/navigation.tsx`
- `apps/desktop/src/renderer/src/platform/navigation.tsx`
- `apps/web/components/web-providers.tsx`
- `apps/desktop/src/renderer/src/App.tsx`
- `pnpm-workspace.yaml`

---

<details>
<summary>Relevant source files</summary>
The following files were used as context for generating this wiki page:
- [packages/core/platform/index.ts](packages/core/platform/index.ts)
- [packages/core/platform/core-provider.tsx](packages/core/platform/core-provider.tsx)
- [packages/core/platform/types.ts](packages/core/platform/types.ts)
- [packages/core/platform/storage.ts](packages/core/platform/storage.ts)
- [packages/core/package.json](packages/core/package.json)
- [packages/core/auth/index.ts](packages/core/auth/index.ts)
- [packages/views/navigation/types.ts](packages/views/navigation/types.ts)
- [packages/views/navigation/context.tsx](packages/views/navigation/context.tsx)
- [packages/views/navigation/index.ts](packages/views/navigation/index.ts)
- [packages/views/package.json](packages/views/package.json)
- [packages/views/platform/index.ts](packages/views/platform/index.ts)
- [apps/web/platform/navigation.tsx](apps/web/platform/navigation.tsx)
- [apps/desktop/src/renderer/src/platform/navigation.tsx](apps/desktop/src/renderer/src/platform/navigation.tsx)
- [apps/web/components/web-providers.tsx](apps/web/components/web-providers.tsx)
- [apps/desktop/src/renderer/src/App.tsx](apps/desktop/src/renderer/src/App.tsx)
- [apps/web/app/layout.tsx](apps/web/app/layout.tsx)
</details>

# The Package Contract: Core, UI, Views

Three packages with iron boundaries let the same business screens, modals, and navigation logic run unchanged in the Next.js web app and the Electron desktop app. `packages/core` owns all server state (React Query), client state (Zustand), the API client, and realtime wiring. It ships as raw TypeScript with no build step. `packages/ui` contains only pure Base UI + shadcn components and never touches `@multica/core`. `packages/views` owns every business page and complex component; it depends on core and ui but is forbidden from importing `next/*` or `react-router-dom`. Routing reaches views through a `NavigationAdapter` interface that each app implements in a one-file platform layer. Both apps wrap their root with the same `CoreProvider`. The result is zero duplication, instant HMR across packages, and a single source of truth for every screen.

## The Internal Packages Pattern

All three packages declare `"type": "module"` and map their `exports` directly to `.ts` and `.tsx` source files. No `dist/`, no `tsc` emit, no pre-bundling. The bundler of the consuming app (Vite in desktop, Next.js compiler in web) type-checks and transforms the files on the fly. This produces zero-config HMR: editing a view component or a core store immediately updates both running apps.

```json
// packages/core/package.json:11-96
"exports": {
  "./auth": "./auth/index.ts",
  "./issues/stores": "./issues/stores/index.ts",
  ...
  "./platform": "./platform/index.ts"
}
```

The same pattern appears in `packages/ui/package.json:10-23` and `packages/views/package.json:11-53`. Because the exports point at source, any app can reach a deep file with a stable subpath import such as `@multica/views/issues/components` or `@multica/core/issues/stores/selection-store`.

Sources: [packages/core/package.json:11-96](), [packages/views/package.json:11-53](), [packages/ui/package.json:10-23]()

## packages/core – The Single Source of Truth

`packages/core` is the only place that may:
- Call the API (`ApiClient`, `api.*`)
- Own Zustand stores (auth, chat, issues/*, agents/*, projects, squads, modals, navigation, onboarding, feedback, runtimes, etc.)
- Own React Query cache and the `QueryClient`
- Manage realtime WebSocket connection
- Define `StorageAdapter`, `LocaleAdapter`, and other platform-neutral contracts

It is strictly forbidden from importing `react-dom`, reading or writing `localStorage` directly, referencing `process.env`, or importing anything from `@multica/ui` or `@multica/views`.

Creation and registration of the critical auth and chat stores happens once inside `CoreProvider`:

```tsx
// packages/core/platform/core-provider.tsx:56-60
authStore = createAuthStore({ api, storage, onLogin, onLogout, cookieAuth });
registerAuthStore(authStore);

chatStore = createChatStore({ storage });
registerChatStore(chatStore);
```

The `register*Store` + Proxy accessor pattern (see `auth/index.ts:16-40`) lets the rest of the codebase consume `useAuthStore` without ever importing the concrete factory. The same module-level singletons survive Vite HMR because they live outside any React component tree.

Platform-specific concerns reach core only through injected adapters (`StorageAdapter`, `LocaleAdapter`, `onLogin`/`onLogout` callbacks, `ClientIdentity`). The default storage implementation (`defaultStorage`) is a thin SSR-safe wrapper; no file in `packages/core` (outside tests) ever touches the global `localStorage` or `process.env`.

Sources: [packages/core/platform/core-provider.tsx:21-63](), [packages/core/auth/index.ts:16-40](), [packages/core/platform/storage.ts:4-13](), [packages/core/platform/types.ts:20-43]()

## packages/ui – Pure Presentational Layer

`packages/ui` exports only:
- shadcn/Base UI components (`components/ui/*`, `components/common/*`)
- Markdown rendering utilities
- Layout primitives, hooks that do not know about business state
- CSS tokens and base styles

A single comment in the entire tree even mentions `@multica/core`; there are zero runtime imports. This guarantees that a designer or a different product can consume the design system in isolation.

Sources: [packages/ui/package.json:10-23](), grep verification across `packages/ui` for `@multica/core` (only a comment at `submit-button.tsx:22`)

## packages/views – Business Screens and Shared Logic

All real product surfaces live here: `dashboard`, `issues/components`, `agents`, `inbox`, `chat`, `settings`, `onboarding`, `modals/*`, `my-issues`, `squads`, `workspace/*` pages, etc. Every one of these files may import from `@multica/core` and `@multica/ui`, but never from `next/*` or `react-router-dom`.

Routing is supplied by the `NavigationProvider` / `useNavigation` pair that views itself owns:

```ts
// packages/views/navigation/types.ts:1-29
export interface NavigationAdapter {
  push(path: string): void;
  replace(path: string): void;
  back(): void;
  pathname: string;
  searchParams: URLSearchParams;
  openInNewTab?(...): void;   // desktop-only extension
  getShareableUrl(path: string): string;
  prefetch?(path: string): void;
}
```

The context (`context.tsx:9-49`) wraps the adapter so that every `AppLink`, command-palette item, and post-mutation redirect calls the same `useNavigation().push` regardless of platform.

Sources: [packages/views/navigation/types.ts:1-29](), [packages/views/navigation/context.tsx:9-49](), [packages/views/navigation/index.ts:1-7]()

## Platform Adapters – The Only Framework Glue

Each app supplies a one-file adapter that implements `NavigationAdapter` and then renders the shared `NavigationProvider`.

**Web** (`apps/web/platform/navigation.tsx:19-33`) reads `useRouter`, `usePathname`, `useSearchParams` from `next/navigation` and forwards them:

```tsx
const adapter: NavigationAdapter = {
  push: router.push,
  replace: router.replace,
  ...
  getShareableUrl: (path) => window.location.origin + path,
  prefetch: router.prefetch,
};
return <NavigationProvider value={adapter}>{children}</NavigationProvider>;
```

**Desktop** (`apps/desktop/src/renderer/src/platform/navigation.tsx:189-240` and `270-308`) is more involved: it intercepts cross-workspace pushes, pinned-tab rules, and overlay transitions (`/workspaces/new`, `/invite/*`) before delegating to the per-tab `DataRouter`. The two navigation providers (`DesktopNavigationProvider` for shell, `TabNavigationProvider` for tab content) keep the same interface that views components already understand.

Both adapters are wrapped after `CoreProvider` at the root:

- Web: `WebProviders` → `CoreProvider` + `WebNavigationProvider` (layout.tsx:139)
- Desktop: `App` → `CoreProvider` + desktop shell that mounts `DesktopNavigationProvider` / `TabNavigationProvider`

Because the adapter is the only place that knows the concrete router, a single `<NewWorkspacePage>` component can be rendered both as a Next.js route (`apps/web/app/(auth)/workspaces/new/page.tsx:9`) and as a `WindowOverlay` child on desktop (`window-overlay.tsx:51`).

Sources: [apps/web/platform/navigation.tsx:19-35](), [apps/desktop/src/renderer/src/platform/navigation.tsx:189-240,270-308](), [apps/web/components/web-providers.tsx:65-91](), [apps/desktop/src/renderer/src/App.tsx:327-337]()

## Dependency Direction and Invariants

```
apps/web          apps/desktop
     \                 /
      v               v
  packages/views  (business screens, NavigationProvider, AppLink)
      |               |
      +---------------+
             |
    +--------+--------+
    |                 |
packages/core     packages/ui
(headless + state)  (pure components)
```

- `core` and `ui` are peers; neither may import the other.
- `views` may import both.
- Only the two thin platform layers in `apps/*` may import framework routing packages.
- All Zustand stores that hold workspace-scoped or business data live in `core`; the single narrow exception (`views/search/search-store.ts`) is a pure UI chrome flag for the command palette and is never used for server data or cross-page coordination.

Violating any arrow immediately breaks one of the two apps or destroys HMR.

Sources: [packages/core/platform/core-provider.tsx:78-81](), package.json exports maps, import greps across `packages/ui` and `packages/views`

## Why the Design Survives HMR and Dual Apps

1. Module-level singletons in core are created exactly once on first `CoreProvider` render and are preserved by Vite's module graph.
2. React Query owns server state; WS events only invalidate queries.
3. Zustand owns ephemeral client state (filters, drafts, selection, modals) and is registered through the same boot path in both apps.
4. Navigation is a pure interface; the adapter can be swapped without touching a single business component.

If the contract were relaxed (for example, allowing `views` to import `next/navigation` directly), the desktop app would either crash on missing hooks or require heavy duplication and conditional imports—the exact problems the three-layer design was built to eliminate.

Sources: [packages/core/platform/core-provider.tsx:21-63](), [apps/web/app/layout.tsx:139-141](), [apps/desktop/src/renderer/src/App.tsx:284-344]()

## Summary

The package contract is the mechanism that lets every screen, modal, and navigation action be written once in `packages/views`, backed by state and API logic that lives only in `packages/core`, rendered with components that live only in `packages/ui`, and executed in two completely different runtime environments through two one-file adapters. The boundaries are not suggestions; they are the reason the product can ship a desktop app that feels native while sharing >95 % of its UI and logic with the web experience, and the reason changes to a shared page survive HMR in both targets without coordination.

Sources: [packages/core/platform/core-provider.tsx:21-124]()
