Admin

Building admin modules

The module contract — directory layout, navigation, page discovery, composables.

Building admin modules

An admin module is a self-contained slice of UI that plugs into the sidebar and routes. It can live in the base layer (for generic modules) or in a project admin (for project-specific ones).

Anatomy

text
modules/<your-module>/
├── module.ts              # navigation + metadata (required)
├── types/index.ts         # wire shapes (optional)
├── composables/
│   └── useYourApi.ts      # typed HTTP wrappers
├── components/            # module-only components, auto-imported globally
│   └── YourWidget.vue
└── pages/<your-module>/   # Vue pages — discovered + mounted
    ├── index.vue          # /your-module
    └── [id].vue           # /your-module/:id

The outer <your-module> appears twice: once as the directory name, once as the route prefix inside pages/. They don't have to match but it's cleaner when they do.

module.ts

ts
export default defineRedelayModule({
  id: 'customers',
  name: 'Customers',
  version: '1.0.0',
  description: 'Customer directory + billing',
  icon: 'i-heroicons-user-group',

  navigation: {
    label: 'Customers',
    icon: 'i-heroicons-user-group',
    group: 'apps',          // 'apps' | 'system' | any group key
    order: 200,             // smaller = higher in the group
    children: [
      { label: 'All',     href: '/customers',          icon: 'i-heroicons-list-bullet',  order: 1 },
      { label: 'Billing', href: '/customers/billing',  icon: 'i-heroicons-credit-card',  order: 2 },
    ],
  },

  permissions: [],
  requiredPermissions: [],

  async onInit() { /* wire permissions, register toast handlers, etc. */ },
})

The sidebar builds itself from every loaded module's navigation entry. Children become a collapsible group; leaf modules with no children render as a single link.

Pages

Every .vue file under pages/<module>/ becomes a route:

FileRoute
pages/customers/index.vue/customers
pages/customers/[id].vue/customers/:id
pages/customers/billing/index.vue/customers/billing
pages/customers/[id]/invoices.vue/customers/:id/invoices

Every page needs:

vue
<script setup>
definePageMeta({
  layout: 'dashboard',
  middleware: ['auth'],
  ssr: false,              // dev-mode reliability, admin is client-rendered anyway
})
</script>

Composables

Put typed HTTP wrappers in composables/. They're auto-imported — no explicit import needed in pages. The globally available $apiFetch (provided by the base layer) handles JWT injection and the /api/v1 prefix:

ts
// modules/customers/composables/useCustomers.ts
import type { Customer, CustomerListResponse } from '../types'

export function useCustomers() {
  async function list(): Promise<CustomerListResponse> {
    return $apiFetch('/api/v1/admin/customers')
  }

  async function get(id: string): Promise<Customer> {
    return $apiFetch(`/api/v1/admin/customers/${encodeURIComponent(id)}`)
  }

  async function create(input: Partial<Customer>): Promise<Customer> {
    return $apiFetch('/api/v1/admin/customers', { method: 'POST', body: input })
  }

  return { list, get, create }
}

Don't import { $apiFetch } from '~/composables/useApiFetch' — the ~ alias resolves to the consuming project, not the base layer. Auto-import Just Works™.

Types

Match the Go struct JSON tags exactly. Go's default is camelCase for struct fields but some modules use snake_case for timestamps — check the live response with curl before drafting types.

ts
// modules/customers/types/index.ts
export interface Customer {
  id: string
  name: string
  email: string
  created_at: string  // snake_case because Go uses bson:"created_at"
  updated_at?: string
}

Components

Files under components/ auto-import globally across the whole admin — they don't need an explicit import in pages either. The EmptyState component in the base layer is a good pattern to copy for anything you want reused.

Page shell

Every page should use the standard dashboard shell for consistency. The shorthand pattern:

vue
<template>
  <div class="flex-1 flex flex-col overflow-hidden">
    <header class="flex-shrink-0 bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 px-4 sm:px-6 py-3 sm:py-4">
      <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
        <div class="min-w-0">
          <h1 class="text-xl sm:text-2xl font-bold">Customers</h1>
          <p class="hidden sm:block text-sm text-gray-600 dark:text-gray-400 mt-1">
            Directory + billing
          </p>
        </div>
        <div class="flex items-center gap-2">
          <UButton color="primary" icon="i-heroicons-plus">New Customer</UButton>
        </div>
      </div>
    </header>

    <main class="flex-1 overflow-y-auto px-4 sm:px-6 py-4 sm:py-6">
      <div class="max-w-6xl mx-auto space-y-6">
        <!-- your UI -->
      </div>
    </main>
  </div>
</template>

See existing modules (users, flows, ai, assistant) for full references.