diff --git a/docs/features/admin-ui-modernization.md b/docs/features/admin-ui-modernization.md
new file mode 100644
index 0000000..9855411
--- /dev/null
+++ b/docs/features/admin-ui-modernization.md
@@ -0,0 +1,957 @@
+---
+title: Admin UI Modernization
+created: 2026-01-22
+updated: 2026-01-22
+tags:
+ - feature
+ - vue
+ - admin
+ - ui
+ - refactor
+status: planning
+priority: high
+---
+
+# Admin UI Modernization
+
+> [!abstract] Summary
+> Migrate the admin dashboard from **React 18 + MUI** to **Vue 3** for consistency with the machine UI, using PrimeVue for components and tRPC for type-safe API communication.
+
+## Quick Links
+
+- [[#Current State]]
+- [[#Why Unify on Vue]]
+- [[#Architecture]]
+- [[#Migration Strategy]]
+- [[#Implementation]]
+
+---
+
+## Current State
+
+### Existing Tech Stack
+
+```
+packages/admin-ui/
+├── src/
+│ ├── pages/ # React page components
+│ ├── components/ # Reusable React components
+│ ├── hooks/ # Custom React hooks
+│ └── ...
+├── package.json # React 18, MUI, Apollo Client
+└── vite.config.js
+```
+
+| Component | Current | Notes |
+|-----------|---------|-------|
+| Framework | React 18.3 | Functional components + hooks |
+| UI Library | MUI v7.1 | Heavy bundle (~300kb) |
+| State | Zustand | Lightweight |
+| API Client | Apollo Client | GraphQL |
+| Styling | Tailwind CSS v4 | Already modern |
+| Build | Vite + SWC | Already modern |
+| Charts | D3 | Keep |
+| Tables | Material React Table | Replace |
+
+### Problems
+
+> [!warning] Issues with Current Setup
+
+1. **Two frameworks** - React (admin) + Vanilla JS (machine) = different mental models
+2. **Heavy bundle** - MUI adds ~300kb to bundle
+3. **GraphQL complexity** - Apollo Client is powerful but heavy
+4. **Inconsistent patterns** - Different state management approaches
+5. **Developer context switching** - Between React and future Vue machine UI
+
+#currentstate
+
+---
+
+## Why Unify on Vue
+
+> [!decision] Single Framework Strategy
+
+### Benefits of Vue Everywhere
+
+| Aspect | Two Frameworks | Single Framework (Vue) |
+|--------|---------------|------------------------|
+| Learning curve | High | Lower |
+| Code sharing | None | Components, composables |
+| Hiring | React + Vue devs | Vue devs only |
+| Maintenance | 2x patterns | 1x patterns |
+| Bundle optimization | Separate | Shared chunks possible |
+
+### Vue 3 vs React 19 for Admin
+
+| Feature | Vue 3 | React 19 |
+|---------|-------|----------|
+| Bundle size | ~33kb | ~42kb |
+| DevTools | Excellent | Excellent |
+| TypeScript | First-class | First-class |
+| Learning curve | Lower | Medium |
+| Component syntax | SFC (cleaner) | JSX |
+| State management | Pinia (simpler) | Context/Zustand |
+
+> [!tip] Strategic Alignment
+> With machine UI moving to Vue 3, unifying admin UI reduces cognitive load and enables shared utilities.
+
+#vue #consistency
+
+---
+
+## Architecture
+
+### Target Stack
+
+```
+┌─────────────────────────────────────────────────────────┐
+│ Admin Dashboard │
+├─────────────────────────────────────────────────────────┤
+│ Vue 3 + TypeScript │
+│ ┌─────────────────────────────────────────────────────┐│
+│ │ Pages (Vue Router) ││
+│ │ ├── DashboardPage.vue ││
+│ │ ├── TransactionsPage.vue ││
+│ │ ├── MachinesPage.vue ││
+│ │ ├── CustomersPage.vue ││
+│ │ ├── MembershipsPage.vue (new) ││
+│ │ └── SettingsPage.vue ││
+│ └─────────────────────────────────────────────────────┘│
+│ ┌─────────────────────────────────────────────────────┐│
+│ │ UI Components (PrimeVue) ││
+│ │ ├── DataTable, Charts, Forms ││
+│ │ └── Custom components ││
+│ └─────────────────────────────────────────────────────┘│
+│ ┌─────────────────────────────────────────────────────┐│
+│ │ State (Pinia) ││
+│ │ ├── useAuthStore ││
+│ │ ├── useMachinesStore ││
+│ │ └── useSettingsStore ││
+│ └─────────────────────────────────────────────────────┘│
+│ ┌─────────────────────────────────────────────────────┐│
+│ │ API Layer (tRPC Client) ││
+│ │ └── Type-safe server communication ││
+│ └─────────────────────────────────────────────────────┘│
+├─────────────────────────────────────────────────────────┤
+│ Tailwind CSS v4 (keep) │
+└─────────────────────────────────────────────────────────┘
+```
+
+### Project Structure
+
+```
+packages/admin-ui/
+├── src/
+│ ├── main.ts # Entry point
+│ ├── App.vue # Root component
+│ ├── router/
+│ │ └── index.ts # Vue Router config
+│ │
+│ ├── pages/ # Route-level components
+│ │ ├── DashboardPage.vue
+│ │ ├── transactions/
+│ │ │ ├── TransactionsPage.vue
+│ │ │ └── TransactionDetailPage.vue
+│ │ ├── machines/
+│ │ │ ├── MachinesPage.vue
+│ │ │ └── MachineDetailPage.vue
+│ │ ├── customers/
+│ │ │ ├── CustomersPage.vue
+│ │ │ └── CustomerDetailPage.vue
+│ │ ├── memberships/
+│ │ │ ├── MembershipsPage.vue
+│ │ │ ├── MembershipDetailPage.vue
+│ │ │ └── TiersConfigPage.vue
+│ │ ├── settings/
+│ │ │ ├── SettingsPage.vue
+│ │ │ ├── WalletSettingsPage.vue
+│ │ │ └── LightningSettingsPage.vue
+│ │ └── auth/
+│ │ ├── LoginPage.vue
+│ │ └── SetupPasskeyPage.vue
+│ │
+│ ├── components/ # Reusable components
+│ │ ├── layout/
+│ │ │ ├── AppSidebar.vue
+│ │ │ ├── AppHeader.vue
+│ │ │ └── AppBreadcrumb.vue
+│ │ ├── common/
+│ │ │ ├── DataTable.vue # Wrapper around PrimeVue
+│ │ │ ├── StatusBadge.vue
+│ │ │ ├── CryptoAmount.vue
+│ │ │ ├── FiatAmount.vue
+│ │ │ └── DateTimeDisplay.vue
+│ │ ├── charts/
+│ │ │ ├── TransactionChart.vue
+│ │ │ ├── VolumeChart.vue
+│ │ │ └── MachineStatusChart.vue
+│ │ ├── machines/
+│ │ │ ├── MachineCard.vue
+│ │ │ ├── MachineStatusIndicator.vue
+│ │ │ └── CassetteStatus.vue
+│ │ └── memberships/
+│ │ ├── MembershipCard.vue
+│ │ ├── TierBadge.vue
+│ │ └── QRCodeGenerator.vue
+│ │
+│ ├── stores/ # Pinia stores
+│ │ ├── auth.ts
+│ │ ├── machines.ts
+│ │ ├── transactions.ts
+│ │ ├── customers.ts
+│ │ ├── memberships.ts
+│ │ └── settings.ts
+│ │
+│ ├── composables/ # Reusable logic
+│ │ ├── useAuth.ts
+│ │ ├── usePagination.ts
+│ │ ├── useFilters.ts
+│ │ ├── useExport.ts
+│ │ └── useNotifications.ts
+│ │
+│ ├── api/ # tRPC client
+│ │ ├── client.ts
+│ │ └── types.ts # Shared types from server
+│ │
+│ ├── types/ # TypeScript types
+│ │ ├── machine.ts
+│ │ ├── transaction.ts
+│ │ ├── customer.ts
+│ │ └── membership.ts
+│ │
+│ ├── utils/ # Utility functions
+│ │ ├── formatters.ts
+│ │ ├── validators.ts
+│ │ └── constants.ts
+│ │
+│ └── styles/ # Global styles
+│ ├── main.css # Tailwind imports
+│ └── primevue-theme.css # PrimeVue customization
+│
+├── index.html
+├── vite.config.ts
+├── tsconfig.json
+├── tailwind.config.ts
+└── package.json
+```
+
+#architecture #structure
+
+---
+
+## UI Component Library
+
+### PrimeVue vs Alternatives
+
+| Library | Bundle | Components | Vue 3 | Tailwind |
+|---------|--------|------------|-------|----------|
+| **PrimeVue** | Tree-shakeable | 90+ | Yes | Yes |
+| Naive UI | ~80kb | 80+ | Yes | Manual |
+| Vuetify 3 | ~300kb | 80+ | Yes | No |
+| Element Plus | ~200kb | 70+ | Yes | No |
+| Headless UI | ~10kb | 10 | Yes | Yes |
+
+> [!decision] PrimeVue
+> - **Tree-shakeable** - Only import what you use
+> - **Tailwind integration** - PrimeVue + Tailwind preset
+> - **Rich data table** - Critical for admin dashboards
+> - **Chart components** - Built-in Chart.js integration
+> - **Accessibility** - WCAG compliant
+> - **Unstyled mode** - Full control with Tailwind
+
+### PrimeVue Setup
+
+```typescript
+// src/main.ts
+import { createApp } from 'vue'
+import PrimeVue from 'primevue/config'
+import Aura from '@primeuix/themes/aura'
+import ToastService from 'primevue/toastservice'
+import ConfirmationService from 'primevue/confirmationservice'
+
+import App from './App.vue'
+import router from './router'
+import { pinia } from './stores'
+
+const app = createApp(App)
+
+app.use(PrimeVue, {
+ theme: {
+ preset: Aura,
+ options: {
+ darkModeSelector: '.dark',
+ cssLayer: {
+ name: 'primevue',
+ order: 'tailwind-base, primevue, tailwind-utilities',
+ },
+ },
+ },
+})
+
+app.use(ToastService)
+app.use(ConfirmationService)
+app.use(router)
+app.use(pinia)
+
+app.mount('#app')
+```
+
+### Component Examples
+
+#### Data Table Wrapper
+
+```vue
+
+
+
+
+
+
+
+
+
+
+
+
+
emit('rowClick', e.data)"
+ >
+
+
+
+
+
+
+
+
+```
+
+#### Transaction List Page
+
+```vue
+
+
+
+
+
+
+```
+
+#primevue #components
+
+---
+
+## API Layer: tRPC
+
+### Why tRPC over GraphQL
+
+| Aspect | GraphQL (Apollo) | tRPC |
+|--------|------------------|------|
+| Bundle size | ~50kb | ~5kb |
+| Type safety | Codegen required | Automatic |
+| Learning curve | Higher | Lower |
+| Caching | Built-in | TanStack Query |
+| Boilerplate | Schema + resolvers | Just functions |
+
+> [!decision] tRPC for Admin API
+> Since both server and admin-ui are TypeScript, tRPC provides end-to-end type safety without code generation.
+
+### tRPC Client Setup
+
+```typescript
+// src/api/client.ts
+import { createTRPCClient, httpBatchLink } from '@trpc/client'
+import type { AppRouter } from '@lamassu/server/trpc'
+import { useAuthStore } from '@/stores/auth'
+
+export const trpc = createTRPCClient({
+ links: [
+ httpBatchLink({
+ url: `${import.meta.env.VITE_API_URL}/trpc`,
+ headers() {
+ const auth = useAuthStore()
+ return {
+ Authorization: auth.token ? `Bearer ${auth.token}` : '',
+ }
+ },
+ }),
+ ],
+})
+```
+
+### Using tRPC in Stores
+
+```typescript
+// src/stores/memberships.ts
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+import { trpc } from '@/api/client'
+import type { Membership, CreateMembershipInput } from '@/types/membership'
+
+export const useMembershipsStore = defineStore('memberships', () => {
+ const memberships = ref([])
+ const loading = ref(false)
+ const error = ref(null)
+
+ async function fetchMemberships(filters?: { tierId?: string; search?: string }) {
+ loading.value = true
+ error.value = null
+ try {
+ // Fully typed! IDE knows exact return type
+ memberships.value = await trpc.membership.list.query(filters)
+ } catch (e) {
+ error.value = e instanceof Error ? e.message : 'Unknown error'
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function createMembership(input: CreateMembershipInput) {
+ // TypeScript ensures input matches server expectations
+ const newMembership = await trpc.membership.create.mutate(input)
+ memberships.value.push(newMembership)
+ return newMembership
+ }
+
+ async function deleteMembership(id: string) {
+ await trpc.membership.delete.mutate({ id })
+ memberships.value = memberships.value.filter(m => m.id !== id)
+ }
+
+ return {
+ memberships,
+ loading,
+ error,
+ fetchMemberships,
+ createMembership,
+ deleteMembership,
+ }
+})
+```
+
+### TanStack Query Integration (Optional)
+
+```typescript
+// For more advanced caching/refetching
+import { useQuery, useMutation, useQueryClient } from '@tanstack/vue-query'
+import { trpc } from '@/api/client'
+
+export function useMemberships(filters?: { tierId?: string }) {
+ return useQuery({
+ queryKey: ['memberships', filters],
+ queryFn: () => trpc.membership.list.query(filters),
+ })
+}
+
+export function useCreateMembership() {
+ const queryClient = useQueryClient()
+
+ return useMutation({
+ mutationFn: (input: CreateMembershipInput) =>
+ trpc.membership.create.mutate(input),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['memberships'] })
+ },
+ })
+}
+```
+
+#trpc #api
+
+---
+
+## Authentication
+
+### Passkey-First Auth Flow
+
+```vue
+
+
+
+
+
+
+
+
+

+
Admin Login
+
+
+
+
+
+
+
+
+
+
+
+ {{ error }}
+
+
+
+
+
or
+
+
+
+
+
+
+
+```
+
+#auth #passkey
+
+---
+
+## Shared Code with Machine UI
+
+### Shared Package Structure
+
+```
+packages/
+├── admin-ui/ # Vue 3 admin dashboard
+├── machine-ui/ # Vue 3 kiosk UI (moved from lamassu-machine)
+└── ui-shared/ # Shared Vue components & utilities
+ ├── src/
+ │ ├── components/
+ │ │ ├── QRCode.vue
+ │ │ ├── CryptoIcon.vue
+ │ │ └── LoadingSpinner.vue
+ │ ├── composables/
+ │ │ ├── useCrypto.ts
+ │ │ ├── useFormatters.ts
+ │ │ └── useValidators.ts
+ │ ├── types/
+ │ │ ├── coin.ts
+ │ │ ├── transaction.ts
+ │ │ └── membership.ts
+ │ └── index.ts
+ └── package.json
+```
+
+### Shared Composables
+
+```typescript
+// packages/ui-shared/src/composables/useFormatters.ts
+import { computed } from 'vue'
+
+export function useFormatters(locale = 'en-US') {
+ const formatFiat = (amount: number, currency: string) => {
+ return new Intl.NumberFormat(locale, {
+ style: 'currency',
+ currency,
+ }).format(amount)
+ }
+
+ const formatCrypto = (sats: number, coin: string) => {
+ if (coin === 'BTC') {
+ return `${(sats / 100_000_000).toFixed(8)} BTC`
+ }
+ return `${sats} sats`
+ }
+
+ const formatDate = (date: Date | string) => {
+ return new Intl.DateTimeFormat(locale, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ }).format(new Date(date))
+ }
+
+ return {
+ formatFiat,
+ formatCrypto,
+ formatDate,
+ }
+}
+```
+
+```typescript
+// Usage in both admin-ui and machine-ui
+import { useFormatters } from '@lamassu/ui-shared'
+
+const { formatFiat, formatCrypto } = useFormatters()
+```
+
+#shared #monorepo
+
+---
+
+## Migration Strategy
+
+### Phase 1: Setup & Infrastructure
+
+> [!todo] Phase 1 Tasks
+
+- [ ] Create new Vue 3 project in `packages/admin-ui-v2/`
+- [ ] Set up Vite + Vue + TypeScript
+- [ ] Configure PrimeVue with Tailwind
+- [ ] Set up Vue Router with auth guards
+- [ ] Set up Pinia stores
+- [ ] Configure tRPC client
+- [ ] Create layout components (Sidebar, Header)
+
+### Phase 2: Core Pages Migration
+
+> [!todo] Phase 2 Tasks
+
+Migrate pages in order of complexity:
+
+1. [ ] Login / Auth pages
+2. [ ] Dashboard (overview)
+3. [ ] Machines list & detail
+4. [ ] Transactions list & detail
+5. [ ] Customers list & detail
+6. [ ] Settings pages
+
+### Phase 3: New Features
+
+> [!todo] Phase 3 Tasks
+
+- [ ] Memberships management (new)
+- [ ] Discount tiers configuration (new)
+- [ ] Lightning/LNbits settings (new)
+- [ ] Enhanced analytics dashboard
+
+### Phase 4: Polish & Cutover
+
+> [!todo] Phase 4 Tasks
+
+- [ ] Dark mode support
+- [ ] Responsive design review
+- [ ] Accessibility audit
+- [ ] Performance optimization
+- [ ] E2E tests with Playwright
+- [ ] Remove old React admin-ui
+- [ ] Update deployment configs
+
+#migration #phases
+
+---
+
+## Build & Deployment
+
+### Vite Config
+
+```typescript
+// packages/admin-ui/vite.config.ts
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import { resolve } from 'path'
+import Components from 'unplugin-vue-components/vite'
+import { PrimeVueResolver } from '@primevue/auto-import-resolver'
+
+export default defineConfig({
+ plugins: [
+ vue(),
+ Components({
+ resolvers: [PrimeVueResolver()],
+ }),
+ ],
+
+ resolve: {
+ alias: {
+ '@': resolve(__dirname, 'src'),
+ },
+ },
+
+ build: {
+ target: 'es2022',
+ outDir: 'dist',
+ sourcemap: true,
+ rollupOptions: {
+ output: {
+ manualChunks: {
+ vue: ['vue', 'vue-router', 'pinia'],
+ primevue: ['primevue'],
+ charts: ['chart.js', 'vue-chartjs'],
+ },
+ },
+ },
+ },
+
+ server: {
+ port: 3001,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ },
+ '/trpc': {
+ target: 'http://localhost:3000',
+ changeOrigin: true,
+ },
+ },
+ },
+})
+```
+
+### Package.json
+
+```json
+{
+ "name": "@lamassu/admin-ui",
+ "version": "2.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vue-tsc --noEmit && vite build",
+ "preview": "vite preview",
+ "test": "vitest",
+ "test:e2e": "playwright test",
+ "lint": "eslint src --ext .vue,.ts --fix",
+ "typecheck": "vue-tsc --noEmit"
+ },
+ "dependencies": {
+ "vue": "^3.5.0",
+ "vue-router": "^4.4.0",
+ "pinia": "^2.2.0",
+ "@primevue/themes": "^4.2.0",
+ "primevue": "^4.2.0",
+ "primeicons": "^7.0.0",
+ "@trpc/client": "^11.0.0",
+ "@tanstack/vue-query": "^5.60.0",
+ "@vueuse/core": "^11.0.0",
+ "@simplewebauthn/browser": "^10.0.0",
+ "chart.js": "^4.4.0",
+ "vue-chartjs": "^5.3.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.1.0",
+ "vite": "^6.0.0",
+ "typescript": "^5.6.0",
+ "vue-tsc": "^2.1.0",
+ "vitest": "^2.1.0",
+ "@vue/test-utils": "^2.4.0",
+ "@playwright/test": "^1.48.0",
+ "tailwindcss": "^4.0.0",
+ "autoprefixer": "^10.4.0",
+ "eslint": "^9.14.0",
+ "eslint-plugin-vue": "^9.30.0",
+ "unplugin-vue-components": "^0.27.0",
+ "@primevue/auto-import-resolver": "^4.2.0"
+ }
+}
+```
+
+#build #deployment
+
+---
+
+## Comparison Summary
+
+| Aspect | Before (React) | After (Vue 3) |
+|--------|---------------|---------------|
+| Framework | React 18 | Vue 3 |
+| UI Library | MUI (~300kb) | PrimeVue (tree-shake) |
+| State | Zustand | Pinia |
+| API | Apollo GraphQL | tRPC |
+| Styling | Tailwind | Tailwind (keep) |
+| Build | Vite | Vite (keep) |
+| Types | TypeScript | TypeScript (keep) |
+| Machine UI | Different (Vanilla) | Same (Vue 3) |
+
+### Bundle Size Targets
+
+| Chunk | Before | After |
+|-------|--------|-------|
+| Framework | ~42kb | ~33kb |
+| UI Library | ~300kb | ~80kb* |
+| API Client | ~50kb | ~5kb |
+| **Total** | **~400kb** | **~120kb** |
+
+*PrimeVue with tree-shaking, only used components
+
+#comparison #summary
+
+---
+
+## Related Documents
+
+- [[machine-ui-modernization]] - Kiosk UI Vue migration
+- [[modernization-plan]] - Overall modernization roadmap
+- [[membership-lightning-integration]] - New membership feature
+- [[lnbits-integration]] - Lightning backend