Skip to content

Frontend Architecture

The frontend is a Vue 3 SPA with TypeScript, built with Vite and installable as a PWA.

State management layers

The frontend uses three complementary layers for state:

1. Pinia stores

Domain-level reactive state. Each store owns one concern:

StoreFilePurpose
authsrc/stores/auth.store.tsCurrent user, session, role detection
tenantsrc/stores/tenant.store.tsCurrent tenant, feature flags
filterssrc/stores/filters.store.tsGlobal filter state (date range, user, project)
layoutsrc/stores/layout.store.tsToast messages and UI state
allEntriessrc/stores/tenant/allEntries.store.tsPaginated entries list
allInvoicessrc/stores/tenant/allInvoices.store.tsInvoices list
pendingEntriessrc/stores/tenant/pendingEntries.store.tsEntries awaiting approval

2. TanStack Query

Used via @tanstack/vue-query for server state — fetching, caching, and synchronising data from the API. Each useQuery call is bound to a cache key and refetches on mount or key change.

3. TanStack DB collections

Used via @tanstack/vue-db for highly reactive, locally-filtered collections. useLiveQuery runs a filter function over a collection and returns a reactive result that updates whenever the underlying data changes — without round-tripping to the server.

Generated SDK

The API client is generated from the OpenAPI spec using @hey-api/openapi-ts. Generated files live in src/api/ and must not be edited manually.

typescript
import { Entries, Invoices, Projects } from '@/api/sdk.gen'

// Typed, auto-complete-friendly API calls
const result = await Entries.entryControllerFindAll({
  path: { tenantId: 'abc' },
  query: { page: 1 },
})

The base client is configured in src/api/client.ts with credentials: 'include' so session cookies are sent automatically.

Authentication client

better-auth's Vue client is initialised in src/lib/auth.ts. The useTypedSession() composable returns the current session and user:

typescript
import { useTypedSession } from '@/lib/session'

const { user, session } = useTypedSession()

Vue Router

Routes are defined in src/router/routes.ts. The router uses two meta flags:

Meta flagEffect
requiresAuth: trueRedirects to /login if no session
requiresSuperAdmin: trueRedirects to / if user is not a super admin

The auth guard is implemented in src/router/index.ts using router.beforeEach.

Feature flags

Feature availability is gated at the route level and within components using the useTenant() store:

typescript
const tenant = useTenant()

if (tenant.hasProjects) { ... }
if (tenant.hasInvoices) { ... }
if (tenant.hasPunchClock) { ... }

See Feature Flags for a full list and their defaults.

PWA

The app is configured as a PWA via vite-plugin-pwa. It can be installed on iOS (Safari → Add to Home Screen) and Android (Chrome → Install App). A service worker caches assets for faster load times and basic offline support.

TT Time Tracker — Internal Documentation