Appearance
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:
| Store | File | Purpose |
|---|---|---|
auth | src/stores/auth.store.ts | Current user, session, role detection |
tenant | src/stores/tenant.store.ts | Current tenant, feature flags |
filters | src/stores/filters.store.ts | Global filter state (date range, user, project) |
layout | src/stores/layout.store.ts | Toast messages and UI state |
allEntries | src/stores/tenant/allEntries.store.ts | Paginated entries list |
allInvoices | src/stores/tenant/allInvoices.store.ts | Invoices list |
pendingEntries | src/stores/tenant/pendingEntries.store.ts | Entries 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 flag | Effect |
|---|---|
requiresAuth: true | Redirects to /login if no session |
requiresSuperAdmin: true | Redirects 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.