> ## Documentation Index
> Fetch the complete documentation index at: https://ship.paralect.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Overview

> apps/web is a TanStack Start SPA — file-based routing, TanStack Query, shadcn/ui and Tailwind v4, with end-to-end type safety from the API.

`apps/web` is Ship's front-end: a [TanStack Start](https://tanstack.com/start) application running in **SPA mode**. It's the same base whether you scaffolded full-stack or web-only — what changes is where the data comes from.

<img src="https://mintcdn.com/ship/UByoaHTxaOwKkFqs/images/dashboards/web-home.png?fit=max&auto=format&n=UByoaHTxaOwKkFqs&q=85&s=97edc0ef6456af1fcad4630126fc072a" alt="The Ship web app" width="2880" height="1920" data-path="images/dashboards/web-home.png" />

## The stack

| Concern       | Tool                                                                                 |
| ------------- | ------------------------------------------------------------------------------------ |
| App framework | [TanStack Start](https://tanstack.com/start) (SPA) on [Vite](https://vite.dev/)      |
| Routing       | [TanStack Router](https://tanstack.com/router) — file-based, in `src/routes/**`      |
| Server state  | [TanStack Query](https://tanstack.com/query)                                         |
| UI            | [shadcn/ui](https://ui.shadcn.com/) + [Tailwind v4](https://tailwindcss.com/)        |
| Forms         | [React Hook Form](https://react-hook-form.com/) + [Zod](https://zod.dev/)            |
| Icons         | [lucide-react](https://lucide.dev/) · [@tabler/icons-react](https://tabler.io/icons) |
| Language      | [TypeScript](https://www.typescriptlang.org/)                                        |

The whole app is wired in [`vite.config.ts`](https://tanstack.com/start) with the `tanstackStart` plugin in `spa` mode, plus `@tailwindcss/vite` and `vite-tsconfig-paths` (the `@/...` alias):

```ts vite.config.ts theme={null}
import tailwindcss from '@tailwindcss/vite';
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import viteReact from '@vitejs/plugin-react';
import { defineConfig } from 'vite';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  server: { port: 3002, strictPort: true },
  plugins: [
    tsconfigPaths(),
    tailwindcss(),
    tanstackStart({
      spa: { enabled: true, prerender: { outputPath: 'index.html' } },
    }),
    viteReact(),
    svgr(),
  ],
});
```

<Info>**SPA mode does not mean "no server."** TanStack Start still runs a server — it just doesn't server-render your routes. That server is where [server functions](/docs/web/server-functions) execute, which is how the web-only shape does its backend work.</Info>

## The root

Everything hangs off `src/routes/__root.tsx`. It defines the document shell, the head (meta, fonts, favicon), and the providers every route shares — TanStack Query, theming, tooltips, and the toaster:

```tsx src/routes/__root.tsx theme={null}
export const Route = createRootRoute({
  ssr: false,
  head: () => ({
    meta: [{ title: 'Ship' }, /* charset, viewport */],
    links: [/* favicon, fonts */],
  }),
  shellComponent: RootDocument,
  component: RootLayout,
});

function RootLayout() {
  return (
    <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
      <QueryClientProvider client={queryClient}>
        <TooltipProvider>
          <Outlet />
        </TooltipProvider>
        <Toaster richColors position="top-right" />
      </QueryClientProvider>
    </ThemeProvider>
  );
}
```

How routes are organised under this root — files, params, guards, loaders — is its own page: [Routing](/docs/web/routing).

## Two ways to get data

The base `apps/web` ships a landing page and nothing else to fetch. How you add data depends on the shape you scaffolded.

<CardGroup cols={2}>
  <Card title="Full-stack" icon="plug">
    Talk to `apps/api` through a fully typed **oRPC client**. The client and the `useApiQuery` / `useApiMutation` / `useApiForm` hooks arrive with the **Auth** plugin, which also adds the sign-in/up pages and the authenticated app shell.
  </Card>

  <Card title="Web-only" icon="server" href="/docs/web/server-functions">
    No separate API. Backend logic runs as **server functions** (`createServerFn`) on the Start server, called straight from a route loader.
  </Card>
</CardGroup>

### Full-stack: the typed oRPC client

Add the Auth plugin and your routes get an `apiClient` whose methods mirror the API contract one-to-one — every input and return type is inferred from the endpoint's `.input()` / `.output()` schemas. Wrap a call in TanStack Query with the plugin's hooks:

```tsx theme={null}
import { apiClient } from '@/services/api-client.service';
import { useApiQuery, useApiMutation, queryKey, useQueryClient } from '@/hooks';

function Notes() {
  const queryClient = useQueryClient();
  const { data: notes = [] } = useApiQuery(apiClient.notes.list);

  const create = useApiMutation(apiClient.notes.create, {
    onSuccess: () => queryClient.invalidateQueries({ queryKey: queryKey(apiClient.notes.list) }),
  });

  return <button onClick={() => create.mutate({ text: 'hi' })}>Add</button>;
}
```

Types flow end-to-end with no codegen and no shared types package: the API emits `.d.ts` (`pnpm --filter api build:types`) and the web app imports them through its `"api": "workspace:*"` dependency. Change an endpoint's output and the client's types change on the next build. See [How Ship works](/docs/how-ship-works).

### Web-only: server functions

With no `apps/api`, your backend is a `createServerFn` handler that runs on the Start server and is consumed by a route loader:

```ts theme={null}
import { createServerFn } from '@tanstack/react-start';

export const getStats = createServerFn({ method: 'GET' }).handler(async () => {
  return { count: 42 };
});
```

Full pattern — loaders, mutations, validation — on the [Server functions](/docs/web/server-functions) page.

## Styling

Tailwind v4 is wired through `@tailwindcss/vite`; tokens live in `src/globals.css`. UI primitives are [shadcn/ui](https://ui.shadcn.com/) components copied into `src/components/ui/**` — your code, edit freely. Conditional classes use the `cn()` helper. See [Styling](/docs/web/styling).

## Run it

```bash theme={null}
pnpm --filter web dev   # web only, http://localhost:3002
pnpm start              # full stack: infra → migrate → api + web
```

## Where things live

```
apps/web/
  vite.config.ts          # TanStack Start (SPA) + Tailwind + svgr + path alias
  src/
    routes/               # file-based routes — __root.tsx + route files
    components/
      ui/                 # shadcn/ui primitives (yours to edit)
    globals.css           # Tailwind v4 tokens
    routeTree.gen.ts      # auto-generated route tree (do not edit)
```

## Next

* [Routing](/docs/web/routing) — file-based routes, params, guards and loaders.
* [Server functions](/docs/web/server-functions) — backend logic for the web-only shape.
* [Styling](/docs/web/styling) — Tailwind v4 and shadcn/ui.
