App Shell

@sometic/app-shell is the System composition package: one createAppShell(...) call wires auth, HTTP, query, head, theme, stores, and forms behind a shared session epoch and a single dispose() graph.

System standout

Sign-out and user switch cannot leave privileged query cache, cross-epoch HTTP replays, or session stores behind. Prefer createAppShell over ad-hoc TanStack + Axios + Helmet + Zustand wiring when you want Sometic’s portable boundaries out of the box.

Overview

ConcernAPI
ComposecreateAppShell({ auth, http?, query?, head?, theme?, stores?, forms?, … })
Epochapp.epoch / app.getEpoch() / app.onEpochChange(listener)
Disposeapp.dispose() tears down binds; disposes owned query/HTTP clients
Auth ↔ querybindQueryToAuth (also applied inside shell)
Auth ↔ HTTPbindAuthToHttp (auth + optional policy interceptors, epoch ledger)
Theme ↔ headbindThemeToHead
Auth ↔ storesbindAuthToStores
Mutation ↔ formbindMutationForm
Query → headbindHeadToQuery
Mutation outboxcreateSessionMutationQueue (in-memory; drops on epoch bump; not durable offline)

When to use

  • One app composition for System packages with shared epoch invalidation
  • Portable apps that already use @sometic/auth + @sometic/http + @sometic/query
  • Guaranteeing logout / user-switch clears privileged client state

When not to use

  • You only need one package (e.g. theme alone): import that package directly
  • Full durable offline queues / data tables: later phases; shell mutation queue is session-lite only
  • Replacing TanStack DevTools or Floating UI: out of scope

Installation

pnpm
pnpm add @sometic/app-shell @sometic/auth @sometic/http @sometic/query
npm
npm install @sometic/app-shell @sometic/auth @sometic/http @sometic/query
yarn
yarn add @sometic/app-shell @sometic/auth @sometic/http @sometic/query
bun
bun add @sometic/app-shell @sometic/auth @sometic/http @sometic/query

Optional peers: @sometic/head, @sometic/theme, @sometic/store, @sometic/forms.

Usage

js
import { createAuth, createMemoryAuthStorage, createTestAuthProvider } from "@sometic/auth";
import { createAppShell } from "@sometic/app-shell";
import { createHeadController } from "@sometic/head";
import { createThemeController } from "@sometic/theme";

const auth = createAuth({
    provider: createTestAuthProvider(),
    storage: createMemoryAuthStorage(),
});
const head = createHeadController();
const theme = createThemeController();

const app = createAppShell({
    auth,
    head,
    theme,
    createHttpOptions: { baseUrl: "https://api.example.com" },
    refetchOnReauth: "all",
    allowAbsoluteUrl: false,
});

console.log(app.epoch);
app.onEpochChange((epoch) => {
    console.log("epoch", epoch);
});

app.dispose();
auth.dispose();
ts
import { createAuth, createMemoryAuthStorage, createTestAuthProvider } from "@sometic/auth";
import { createAppShell, type AppShell } from "@sometic/app-shell";
import { createHeadController } from "@sometic/head";
import { createThemeController } from "@sometic/theme";

const auth = createAuth({
    provider: createTestAuthProvider(),
    storage: createMemoryAuthStorage(),
});
const head = createHeadController();
const theme = createThemeController();

const app: AppShell = createAppShell({
    auth,
    head,
    theme,
    createHttpOptions: { baseUrl: "https://api.example.com" },
    refetchOnReauth: "all",
    allowAbsoluteUrl: false,
    maxResponseBytes: 2_000_000,
});

const stop = app.onEpochChange((epoch: number) => {
    void epoch;
});
stop();
app.dispose();
auth.dispose();
js
import { createAuth, createMemoryAuthStorage, createTestAuthProvider } from "@sometic/auth";
import { createAppShell } from "@sometic/app-shell";
import { applyHead, createHeadController } from "@sometic/head";
import { applyThemeToElement, createThemeController } from "@sometic/theme";

const auth = createAuth({
    provider: createTestAuthProvider(),
    storage: createMemoryAuthStorage(),
});
const head = createHeadController({ initial: { title: "App" } });
const theme = createThemeController();
theme.subscribe(() => {
    applyThemeToElement(document.documentElement, theme.get());
});
head.subscribe(() => {
    applyHead(document, head.get());
});

const app = createAppShell({ auth, head, theme });
document.querySelector("[data-sign-out]")?.addEventListener("click", () => {
    void auth.signOut();
});
window.addEventListener("pagehide", () => {
    app.dispose();
    auth.dispose();
});

Boundaries (enforced by design)

DataPackageShell behavior
Session / identity@sometic/authEpoch source of truth
Server lists / detail@sometic/queryCleared on epoch bump; refetch after re-auth
Transport@sometic/httpEpoch tagged; cross-epoch replay refused
Client UI / prefs@sometic/storeSession stores reset; prefs optional
Form drafts@sometic/formsNever parked in query; omit secrets from drafts
Document head@sometic/headTheme bind + optional query → SEO patches

Options

InputBehavior
authRequired
http / create optionsAttach auth + policy + epoch interceptors
query / create optionsbindQueryToAuth
head / themeOptional; bindThemeToHead when both present
stores{ ui?, prefs?, session? }; session stores reset on epoch
forms{ draftsClearOnEpoch?, register? }
refetchOnReauth'auth' | 'all' | false
authQueryKeysUsed when refetchOnReauth: 'auth'
allowAbsoluteUrl / maxResponseBytesForwarded to HTTP when shell creates the client

FAQ

Why App Shell instead of wiring TanStack + Axios + Helmet + Zustand myself?

You can wire those tools. App Shell exists so session epoch, query clear, HTTP replay refusal, theme↔head, and mutation↔form share one dispose graph and one mental model across Vanilla, React, and Vue, without inventing the glue in every app.

Does App Shell replace my router or UI kit?

No. It composes Sometic System packages. Routing, layouts, and visual design stay yours.

Is the mutation queue offline-durable?

No. createSessionMutationQueue drops on epoch change. Full offline queues are a later phase.

Who owns dispose?

Caller-owned auth / head / theme / passed-in query are not disposed by the shell (unless ownQuery). HTTP/query clients created by the shell are disposed with app.dispose().