Skip to main content
A service in apps/web is a small module under @/services that wraps an external system behind a typed interface. The base web app ships three, all re-exported from @/services:
The oRPC API client is delivered by the Auth plugin (auth-starter) alongside the hooks and the authenticated shell. The socket and analytics services live in the base web app.

API client

The API client is the oRPC client, in @/services/api-client.service.ts. It’s typed by the API’s exported AppClient, so calls are fully checked end to end. There is no axios wrapper and no separate client class to construct.
Call it directly, or through the useApiQuery / useApiMutation hooks for TanStack Query integration:
credentials: 'include' ships the better-auth session cookie with every request, and the client exposes an ORPC_PATH symbol so query keys derive straight from the procedure — covered in Calling the API.

Socket service

@/services/socket.service.ts wraps a Socket.IO client pointed at config.WS_URL. It connects lazily (autoConnect: false) over the WebSocket transport and exposes a small surface:
Subscribe at module scope, not in a useEffect — listeners are wired once when the module loads and push into the TanStack Query cache. This is how useCurrentUser stays live:
Ship enforces a no-useEffect rule (there’s a skill for it). Socket listeners that feed the cache live at module scope, like the snippet above — not inside component effects.
Connection-time subscriptions live in @/services/socket-handlers.ts. On connect, it reads the cached user and joins that user’s room so server-side mutation handlers can target them:

Analytics service

@/services/analytics.service.ts wraps Mixpanel, guarded by config.MIXPANEL_API_KEY (optional — analytics is a no-op when it’s unset).
track swallows its own errors, so a failed analytics call never breaks a user flow. Swap Mixpanel for another provider by editing this one file — like every Ship service, it’s yours to change.

Config and env

Services read configuration from config, a validated, typed object. Client env vars use the VITE_ prefix and are read via import.meta.env:
Add a new external integration by dropping another module in @/services and re-exporting it from @/services/index.ts.