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
| Concern | API |
|---|---|
| Compose | createAppShell({ auth, http?, query?, head?, theme?, stores?, forms?, … }) |
| Epoch | app.epoch / app.getEpoch() / app.onEpochChange(listener) |
| Dispose | app.dispose() tears down binds; disposes owned query/HTTP clients |
| Auth ↔ query | bindQueryToAuth (also applied inside shell) |
| Auth ↔ HTTP | bindAuthToHttp (auth + optional policy interceptors, epoch ledger) |
| Theme ↔ head | bindThemeToHead |
| Auth ↔ stores | bindAuthToStores |
| Mutation ↔ form | bindMutationForm |
| Query → head | bindHeadToQuery |
| Mutation outbox | createSessionMutationQueue (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 add @sometic/app-shell @sometic/auth @sometic/http @sometic/querynpm install @sometic/app-shell @sometic/auth @sometic/http @sometic/queryyarn add @sometic/app-shell @sometic/auth @sometic/http @sometic/querybun add @sometic/app-shell @sometic/auth @sometic/http @sometic/queryOptional peers: @sometic/head, @sometic/theme, @sometic/store, @sometic/forms.
Usage
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();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();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)
| Data | Package | Shell behavior |
|---|---|---|
| Session / identity | @sometic/auth | Epoch source of truth |
| Server lists / detail | @sometic/query | Cleared on epoch bump; refetch after re-auth |
| Transport | @sometic/http | Epoch tagged; cross-epoch replay refused |
| Client UI / prefs | @sometic/store | Session stores reset; prefs optional |
| Form drafts | @sometic/forms | Never parked in query; omit secrets from drafts |
| Document head | @sometic/head | Theme bind + optional query → SEO patches |
Options
| Input | Behavior |
|---|---|
auth | Required |
http / create options | Attach auth + policy + epoch interceptors |
query / create options | bindQueryToAuth |
head / theme | Optional; bindThemeToHead when both present |
stores | { ui?, prefs?, session? }; session stores reset on epoch |
forms | { draftsClearOnEpoch?, register? } |
refetchOnReauth | 'auth' | 'all' | false |
authQueryKeys | Used when refetchOnReauth: 'auth' |
allowAbsoluteUrl / maxResponseBytes | Forwarded 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().