Building admin modules
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
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
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:
| File | Route |
|---|---|
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:
<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:
// 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.
// 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:
<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.